mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
1912 lines
69 KiB
QML
1912 lines
69 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import qs.Commons
|
|
import qs.Modules.MainScreen
|
|
import qs.Modules.Panels.Settings
|
|
import qs.Services.Theming
|
|
import qs.Services.UI
|
|
import qs.Widgets
|
|
|
|
SmartPanel {
|
|
id: root
|
|
|
|
preferredWidth: 800 * Style.uiScaleRatio
|
|
preferredHeight: 600 * Style.uiScaleRatio
|
|
preferredWidthRatio: 0.5
|
|
preferredHeightRatio: 0.7
|
|
|
|
// Positioning
|
|
readonly property string screenBarPosition: Settings.getBarPositionForScreen(screen?.name)
|
|
readonly property string panelPosition: {
|
|
if (Settings.data.wallpaper.panelPosition === "follow_bar") {
|
|
if (screenBarPosition === "left" || screenBarPosition === "right") {
|
|
return `center_${screenBarPosition}`;
|
|
} else {
|
|
return `${screenBarPosition}_center`;
|
|
}
|
|
} else {
|
|
return Settings.data.wallpaper.panelPosition;
|
|
}
|
|
}
|
|
panelAnchorHorizontalCenter: panelPosition === "center" || panelPosition.endsWith("_center")
|
|
panelAnchorVerticalCenter: panelPosition === "center"
|
|
panelAnchorLeft: panelPosition !== "center" && panelPosition.endsWith("_left")
|
|
panelAnchorRight: panelPosition !== "center" && panelPosition.endsWith("_right")
|
|
panelAnchorBottom: panelPosition.startsWith("bottom_")
|
|
panelAnchorTop: panelPosition.startsWith("top_")
|
|
|
|
// Store direct reference to content for instant access
|
|
property var contentItem: null
|
|
|
|
// Override keyboard handlers to enable grid navigation
|
|
function onDownPressed() {
|
|
if (!contentItem)
|
|
return;
|
|
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex);
|
|
if (view?.gridView) {
|
|
if (!view.gridView.hasActiveFocus) {
|
|
view.gridView.forceActiveFocus();
|
|
if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) {
|
|
view.gridView.currentIndex = 0;
|
|
}
|
|
} else {
|
|
if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) {
|
|
view.gridView.currentIndex = 0;
|
|
} else {
|
|
view.gridView.moveCurrentIndexDown();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function onUpPressed() {
|
|
if (!contentItem)
|
|
return;
|
|
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex);
|
|
if (view?.gridView?.hasActiveFocus) {
|
|
if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) {
|
|
view.gridView.currentIndex = 0;
|
|
} else {
|
|
view.gridView.moveCurrentIndexUp();
|
|
}
|
|
}
|
|
}
|
|
|
|
function onLeftPressed() {
|
|
if (!contentItem)
|
|
return;
|
|
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex);
|
|
if (view?.gridView?.hasActiveFocus) {
|
|
if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) {
|
|
view.gridView.currentIndex = 0;
|
|
} else {
|
|
view.gridView.moveCurrentIndexLeft();
|
|
}
|
|
}
|
|
}
|
|
|
|
function onRightPressed() {
|
|
if (!contentItem)
|
|
return;
|
|
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex);
|
|
if (view?.gridView?.hasActiveFocus) {
|
|
if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) {
|
|
view.gridView.currentIndex = 0;
|
|
} else {
|
|
view.gridView.moveCurrentIndexRight();
|
|
}
|
|
}
|
|
}
|
|
|
|
function onReturnPressed() {
|
|
if (!contentItem)
|
|
return;
|
|
|
|
// Check if Wallhaven page input has focus
|
|
if (contentItem.wallhavenView && contentItem.wallhavenView.visible && contentItem.wallhavenView.pageInput && contentItem.wallhavenView.pageInput.inputItem.activeFocus) {
|
|
contentItem.wallhavenView.pageInput.submitPage();
|
|
return;
|
|
}
|
|
|
|
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex);
|
|
if (view?.gridView?.hasActiveFocus) {
|
|
let gridView = view.gridView;
|
|
if (gridView.currentIndex >= 0 && gridView.currentIndex < gridView.model.count) {
|
|
var item = gridView.model.get(gridView.currentIndex);
|
|
view.selectItem(item.path, item.isDirectory);
|
|
}
|
|
}
|
|
}
|
|
|
|
function onEnterPressed() {
|
|
onReturnPressed();
|
|
}
|
|
|
|
panelContent: Rectangle {
|
|
id: panelContent
|
|
|
|
property alias wallhavenView: wallhavenView
|
|
property int currentScreenIndex: {
|
|
if (screen !== null) {
|
|
for (var i = 0; i < Quickshell.screens.length; i++) {
|
|
if (Quickshell.screens[i].name == screen.name) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
property var currentScreen: Quickshell.screens[currentScreenIndex]
|
|
property string filterText: ""
|
|
property int appearanceTabIndex: 0
|
|
property alias screenRepeater: screenRepeater
|
|
|
|
Component.onCompleted: {
|
|
root.contentItem = panelContent;
|
|
}
|
|
|
|
// Function to update Wallhaven resolution filter
|
|
function updateWallhavenResolution() {
|
|
if (typeof WallhavenService === "undefined") {
|
|
return;
|
|
}
|
|
|
|
var width = Settings.data.wallpaper.wallhavenResolutionWidth || "";
|
|
var height = Settings.data.wallpaper.wallhavenResolutionHeight || "";
|
|
var mode = Settings.data.wallpaper.wallhavenResolutionMode || "atleast";
|
|
|
|
if (width && height) {
|
|
var resolution = width + "x" + height;
|
|
if (mode === "atleast") {
|
|
WallhavenService.minResolution = resolution;
|
|
WallhavenService.resolutions = "";
|
|
} else {
|
|
WallhavenService.minResolution = "";
|
|
WallhavenService.resolutions = resolution;
|
|
}
|
|
} else {
|
|
WallhavenService.minResolution = "";
|
|
WallhavenService.resolutions = "";
|
|
}
|
|
|
|
// Trigger new search with updated resolution
|
|
if (Settings.data.wallpaper.useWallhaven) {
|
|
if (wallhavenView) {
|
|
wallhavenView.loading = true;
|
|
}
|
|
WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", 1);
|
|
}
|
|
}
|
|
|
|
color: "transparent"
|
|
|
|
// Wallhaven settings popup
|
|
Loader {
|
|
id: wallhavenSettingsPopup
|
|
source: "WallhavenSettingsPopup.qml"
|
|
onLoaded: {
|
|
if (item) {
|
|
item.screen = screen;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Solid color picker dialog
|
|
NColorPickerDialog {
|
|
id: solidColorPicker
|
|
screen: root.screen
|
|
selectedColor: Settings.data.wallpaper.solidColor
|
|
onColorSelected: color => WallpaperService.setSolidColor(color.toString())
|
|
}
|
|
|
|
// Focus management
|
|
Connections {
|
|
target: root
|
|
function onOpened() {
|
|
// Ensure contentItem is set
|
|
if (!root.contentItem) {
|
|
root.contentItem = panelContent;
|
|
}
|
|
// Reset grid view selections
|
|
for (var i = 0; i < screenRepeater.count; i++) {
|
|
let item = screenRepeater.itemAt(i);
|
|
if (item && item.gridView) {
|
|
item.gridView.currentIndex = -1;
|
|
}
|
|
}
|
|
if (wallhavenView && wallhavenView.gridView) {
|
|
wallhavenView.gridView.currentIndex = -1;
|
|
}
|
|
panelContent.appearanceTabIndex = Settings.data.colorSchemes.darkMode ? 1 : 0;
|
|
WallpaperService.wallpaperSelectionAppearance = panelContent.appearanceTabIndex === 1 ? "dark" : "light";
|
|
// Give initial focus to search input
|
|
Qt.callLater(() => {
|
|
if (searchInput.inputItem) {
|
|
searchInput.inputItem.forceActiveFocus();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Debounce timer for search
|
|
Timer {
|
|
id: searchDebounceTimer
|
|
interval: 150
|
|
onTriggered: {
|
|
panelContent.filterText = searchInput.text;
|
|
// Trigger update on all screen views
|
|
for (var i = 0; i < screenRepeater.count; i++) {
|
|
let item = screenRepeater.itemAt(i);
|
|
if (item && item.updateFiltered) {
|
|
item.updateFiltered();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginL
|
|
spacing: Style.marginM
|
|
|
|
// Debounce timer for Wallhaven search
|
|
Timer {
|
|
id: wallhavenSearchDebounceTimer
|
|
interval: 500
|
|
onTriggered: {
|
|
Settings.data.wallpaper.wallhavenQuery = searchInput.text;
|
|
if (typeof WallhavenService !== "undefined") {
|
|
wallhavenView.loading = true;
|
|
WallhavenService.search(searchInput.text, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Header
|
|
NBox {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: headerColumn.implicitHeight + Style.margin2L
|
|
color: Color.mSurfaceVariant
|
|
|
|
ColumnLayout {
|
|
id: headerColumn
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginL
|
|
spacing: Style.marginM
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.marginM
|
|
|
|
NIcon {
|
|
icon: "settings-wallpaper-selector"
|
|
pointSize: Style.fontSizeXXL
|
|
color: Color.mPrimary
|
|
}
|
|
|
|
NText {
|
|
text: I18n.tr("wallpaper.panel.title")
|
|
pointSize: Style.fontSizeL
|
|
font.weight: Style.fontWeightBold
|
|
color: Color.mOnSurface
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
NIconButton {
|
|
icon: "palette"
|
|
tooltipText: I18n.tr("wallpaper.panel.solid-color-tooltip")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
colorBg: Settings.data.wallpaper.useSolidColor ? Color.mPrimary : Color.mSurfaceVariant
|
|
colorFg: Settings.data.wallpaper.useSolidColor ? Color.mOnPrimary : Color.mPrimary
|
|
onClicked: solidColorPicker.open()
|
|
}
|
|
|
|
NIconButton {
|
|
icon: "settings"
|
|
tooltipText: I18n.tr("panels.wallpaper.settings-title")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
onClicked: {
|
|
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
|
|
settingsPanel.requestedTab = SettingsPanel.Tab.Wallpaper;
|
|
settingsPanel.open();
|
|
}
|
|
}
|
|
|
|
NIconButton {
|
|
icon: "close"
|
|
tooltipText: I18n.tr("common.close")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
onClicked: root.close()
|
|
}
|
|
}
|
|
|
|
NDivider {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
NToggle {
|
|
label: I18n.tr("wallpaper.panel.apply-all-monitors-label")
|
|
description: I18n.tr("wallpaper.panel.apply-all-monitors-description")
|
|
checked: Settings.data.wallpaper.setWallpaperOnAllMonitors
|
|
onToggled: checked => Settings.data.wallpaper.setWallpaperOnAllMonitors = checked
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
NTabBar {
|
|
id: appearanceTabBar
|
|
visible: Settings.data.wallpaper.enabled
|
|
Layout.fillWidth: true
|
|
currentIndex: panelContent.appearanceTabIndex
|
|
spacing: Style.marginM
|
|
distributeEvenly: true
|
|
|
|
onCurrentIndexChanged: {
|
|
if (currentIndex < 0) {
|
|
return;
|
|
}
|
|
panelContent.appearanceTabIndex = currentIndex;
|
|
WallpaperService.wallpaperSelectionAppearance = currentIndex === 1 ? "dark" : "light";
|
|
Settings.data.colorSchemes.darkMode = currentIndex === 1;
|
|
}
|
|
|
|
NTabButton {
|
|
text: I18n.tr("wallpaper.panel.appearance-light-tab")
|
|
tabIndex: 0
|
|
checked: appearanceTabBar.currentIndex === 0
|
|
}
|
|
NTabButton {
|
|
text: I18n.tr("wallpaper.panel.appearance-dark-tab")
|
|
tabIndex: 1
|
|
checked: appearanceTabBar.currentIndex === 1
|
|
}
|
|
}
|
|
|
|
// Monitor tabs
|
|
NTabBar {
|
|
id: screenTabBar
|
|
visible: (!Settings.data.wallpaper.setWallpaperOnAllMonitors || Settings.data.wallpaper.enableMultiMonitorDirectories)
|
|
Layout.fillWidth: true
|
|
currentIndex: currentScreenIndex
|
|
onCurrentIndexChanged: currentScreenIndex = currentIndex
|
|
spacing: Style.marginM
|
|
distributeEvenly: true
|
|
|
|
Repeater {
|
|
model: Quickshell.screens
|
|
NTabButton {
|
|
required property var modelData
|
|
required property int index
|
|
text: modelData.name || `Screen ${index + 1}`
|
|
tabIndex: index
|
|
checked: {
|
|
screenTabBar.currentIndex === index;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unified search input and source
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.marginM
|
|
|
|
NTextInput {
|
|
id: searchInput
|
|
placeholderText: Settings.data.wallpaper.useWallhaven ? I18n.tr("placeholders.search-wallhaven") : I18n.tr("placeholders.search-wallpapers")
|
|
fontSize: Style.fontSizeM
|
|
Layout.fillWidth: true
|
|
|
|
property bool initializing: true
|
|
Component.onCompleted: {
|
|
// Initialize text based on current mode
|
|
if (Settings.data.wallpaper.useWallhaven) {
|
|
searchInput.text = Settings.data.wallpaper.wallhavenQuery || "";
|
|
} else {
|
|
searchInput.text = panelContent.filterText || "";
|
|
}
|
|
// Give focus to search input
|
|
if (searchInput.inputItem && searchInput.inputItem.visible) {
|
|
searchInput.inputItem.forceActiveFocus();
|
|
}
|
|
// Mark initialization as complete after a short delay
|
|
Qt.callLater(function () {
|
|
searchInput.initializing = false;
|
|
});
|
|
}
|
|
|
|
Connections {
|
|
target: Settings.data.wallpaper
|
|
function onUseWallhavenChanged() {
|
|
// Update text when mode changes
|
|
if (Settings.data.wallpaper.useWallhaven) {
|
|
searchInput.text = Settings.data.wallpaper.wallhavenQuery || "";
|
|
} else {
|
|
searchInput.text = panelContent.filterText || "";
|
|
}
|
|
}
|
|
}
|
|
|
|
onTextChanged: {
|
|
// Don't trigger search during initialization - Component.onCompleted will handle initial search
|
|
if (initializing) {
|
|
return;
|
|
}
|
|
if (Settings.data.wallpaper.useWallhaven) {
|
|
wallhavenSearchDebounceTimer.restart();
|
|
} else {
|
|
searchDebounceTimer.restart();
|
|
}
|
|
}
|
|
|
|
onEditingFinished: {
|
|
if (Settings.data.wallpaper.useWallhaven) {
|
|
wallhavenSearchDebounceTimer.stop();
|
|
// Only search if the query actually changed
|
|
if (typeof WallhavenService !== "undefined" && text !== WallhavenService.currentQuery) {
|
|
Settings.data.wallpaper.wallhavenQuery = text;
|
|
wallhavenView.loading = true;
|
|
WallhavenService.search(text, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
Keys.onPressed: event => {
|
|
if (Keybinds.checkKey(event, 'down', Settings)) {
|
|
if (Settings.data.wallpaper.useWallhaven) {
|
|
if (wallhavenView && wallhavenView.gridView) {
|
|
wallhavenView.gridView.forceActiveFocus();
|
|
}
|
|
} else {
|
|
let currentView = screenRepeater.itemAt(currentScreenIndex);
|
|
if (currentView && currentView.gridView) {
|
|
currentView.gridView.forceActiveFocus();
|
|
}
|
|
}
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
NIconButton {
|
|
icon: "color-swatch"
|
|
tooltipText: Settings.data.colorSchemes.useWallpaperColors ? I18n.tr("wallpaper.panel.color-extraction-enabled") : I18n.tr("wallpaper.panel.color-extraction-disabled")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
onClicked: {
|
|
Settings.data.colorSchemes.useWallpaperColors = !Settings.data.colorSchemes.useWallpaperColors;
|
|
if (Settings.data.colorSchemes.useWallpaperColors) {
|
|
AppThemeService.generate();
|
|
} else {
|
|
ColorSchemeService.setPredefinedScheme(Settings.data.colorSchemes.predefinedScheme);
|
|
}
|
|
}
|
|
}
|
|
|
|
NComboBox {
|
|
id: colorSchemeComboBox
|
|
Layout.fillWidth: false
|
|
Layout.minimumWidth: 200
|
|
minimumWidth: 200
|
|
|
|
property bool _initialized: false
|
|
property bool _userChanging: false
|
|
Component.onCompleted: Qt.callLater(() => {
|
|
_initialized = true;
|
|
})
|
|
|
|
model: Settings.data.colorSchemes.useWallpaperColors ? TemplateProcessor.schemeTypes : ColorSchemeService.schemes.map(s => ({
|
|
"key": ColorSchemeService.getBasename(s),
|
|
"name": ColorSchemeService.getBasename(s)
|
|
}))
|
|
currentKey: Settings.data.colorSchemes.useWallpaperColors ? Settings.data.colorSchemes.generationMethod : Settings.data.colorSchemes.predefinedScheme
|
|
onCurrentKeyChanged: {
|
|
if (!_initialized)
|
|
return;
|
|
if (_userChanging) {
|
|
_userChanging = false;
|
|
return;
|
|
}
|
|
schemeGlowAnimation.restart();
|
|
}
|
|
onSelected: key => {
|
|
_userChanging = true;
|
|
if (Settings.data.colorSchemes.useWallpaperColors) {
|
|
Settings.data.colorSchemes.generationMethod = key;
|
|
AppThemeService.generate();
|
|
} else {
|
|
ColorSchemeService.setPredefinedScheme(key);
|
|
}
|
|
Qt.callLater(() => {
|
|
_userChanging = false;
|
|
});
|
|
}
|
|
|
|
SequentialAnimation {
|
|
id: schemeGlowAnimation
|
|
NumberAnimation {
|
|
target: colorSchemeComboBox
|
|
property: "opacity"
|
|
to: 0.3
|
|
duration: Style.animationSlow
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
NumberAnimation {
|
|
target: colorSchemeComboBox
|
|
property: "opacity"
|
|
to: 1.0
|
|
duration: Style.animationSlow
|
|
easing.type: Easing.InCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
NComboBox {
|
|
id: sourceComboBox
|
|
Layout.fillWidth: false
|
|
|
|
model: [
|
|
{
|
|
"key": "local",
|
|
"name": I18n.tr("common.local")
|
|
},
|
|
{
|
|
"key": "wallhaven",
|
|
"name": I18n.tr("wallpaper.panel.source-wallhaven")
|
|
}
|
|
]
|
|
currentKey: Settings.data.wallpaper.useWallhaven ? "wallhaven" : "local"
|
|
property bool skipNextSelected: false
|
|
Component.onCompleted: {
|
|
// Skip the first onSelected if it fires during initialization
|
|
skipNextSelected = true;
|
|
Qt.callLater(function () {
|
|
skipNextSelected = false;
|
|
});
|
|
}
|
|
onSelected: key => {
|
|
if (skipNextSelected) {
|
|
return;
|
|
}
|
|
var useWallhaven = (key === "wallhaven");
|
|
Settings.data.wallpaper.useWallhaven = useWallhaven;
|
|
// Update search input text based on mode
|
|
if (useWallhaven) {
|
|
searchInput.text = Settings.data.wallpaper.wallhavenQuery || "";
|
|
} else {
|
|
searchInput.text = panelContent.filterText || "";
|
|
}
|
|
if (useWallhaven && typeof WallhavenService !== "undefined") {
|
|
// Update service properties when switching to Wallhaven
|
|
// Don't search here - Component.onCompleted will handle it when the component is created
|
|
// This prevents duplicate searches
|
|
WallhavenService.categories = Settings.data.wallpaper.wallhavenCategories;
|
|
WallhavenService.purity = Settings.data.wallpaper.wallhavenPurity;
|
|
WallhavenService.sorting = Settings.data.wallpaper.wallhavenSorting;
|
|
WallhavenService.order = Settings.data.wallpaper.wallhavenOrder;
|
|
|
|
// Update resolution settings
|
|
panelContent.updateWallhavenResolution();
|
|
|
|
// If the view is already initialized, trigger a new search when switching to it
|
|
// Preserve current page when switching back to Wallhaven source
|
|
if (wallhavenView && wallhavenView.initialized && !WallhavenService.fetching) {
|
|
wallhavenView.loading = true;
|
|
WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", WallhavenService.currentPage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Settings button (only visible for Wallhaven)
|
|
NIconButton {
|
|
id: wallhavenSettingsButton
|
|
icon: "settings"
|
|
tooltipText: I18n.tr("wallpaper.panel.wallhaven-settings-title")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
visible: Settings.data.wallpaper.useWallhaven
|
|
onClicked: {
|
|
if (searchInput.inputItem) {
|
|
searchInput.inputItem.focus = false;
|
|
}
|
|
if (wallhavenSettingsPopup.item) {
|
|
wallhavenSettingsPopup.item.showAt(wallhavenSettingsButton);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Content stack: Wallhaven or Local
|
|
NBox {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
color: Color.mSurfaceVariant
|
|
|
|
StackLayout {
|
|
id: contentStack
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginL
|
|
|
|
currentIndex: Settings.data.wallpaper.useWallhaven ? 1 : 0
|
|
|
|
// Local wallpapers
|
|
StackLayout {
|
|
id: screenStack
|
|
currentIndex: currentScreenIndex
|
|
|
|
Repeater {
|
|
id: screenRepeater
|
|
model: Quickshell.screens
|
|
delegate: WallpaperScreenView {
|
|
targetScreen: modelData
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wallhaven wallpapers
|
|
WallhavenView {
|
|
id: wallhavenView
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Component for each screen's wallpaper view
|
|
component WallpaperScreenView: Item {
|
|
property var targetScreen
|
|
property alias gridView: wallpaperGridView
|
|
|
|
// Local reactive state for this screen
|
|
property list<string> wallpapersList: []
|
|
property string currentWallpaper: ""
|
|
property var filteredItems: [] // Combined list of { path, name, isDirectory }
|
|
property var wallpapersWithNames: [] // Cached basenames for files
|
|
property var directoriesList: [] // List of directories in browse mode
|
|
|
|
// ListModel for the grid — enables animated reordering via move()
|
|
ListModel {
|
|
id: wallpaperModel
|
|
}
|
|
|
|
// Browse mode properties
|
|
property string currentBrowsePath: WallpaperService.getCurrentBrowsePath(targetScreen?.name ?? "")
|
|
property bool isBrowseMode: Settings.data.wallpaper.viewMode === "browse"
|
|
property int _browseScanGeneration: 0
|
|
|
|
// Order: (1) favorites for the active Light/Dark tab, (2) favorites only for the other appearance,
|
|
// (3) everything else. Legacy favorites without "appearance" still match via WallpaperService (darkMode on entry).
|
|
function sortFavoritesToTop(items) {
|
|
var forThisAppearance = [];
|
|
var forOtherAppearance = [];
|
|
var rest = [];
|
|
var app = WallpaperService.wallpaperSelectionAppearance;
|
|
for (var i = 0; i < items.length; i++) {
|
|
var it = items[i];
|
|
if (!it.isDirectory) {
|
|
var path = it.path;
|
|
var here = WallpaperService.isFavorite(path, app);
|
|
var any = WallpaperService.isFavorite(path);
|
|
if (here) {
|
|
forThisAppearance.push(it);
|
|
} else if (any) {
|
|
forOtherAppearance.push(it);
|
|
} else {
|
|
rest.push(it);
|
|
}
|
|
} else {
|
|
rest.push(it);
|
|
}
|
|
}
|
|
return forThisAppearance.concat(forOtherAppearance).concat(rest);
|
|
}
|
|
|
|
// Rebuild filteredItems and sync to wallpaperModel (full replacement, no animation).
|
|
// When skipSync is true the caller will animate the model itself.
|
|
function updateFiltered(skipSync) {
|
|
var combinedItems = [];
|
|
|
|
// In browse mode, add directories first
|
|
if (isBrowseMode) {
|
|
for (var i = 0; i < directoriesList.length; i++) {
|
|
var dirPath = directoriesList[i];
|
|
combinedItems.push({
|
|
"path": dirPath,
|
|
"name": dirPath.split('/').pop(),
|
|
"isDirectory": true
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add files
|
|
for (var i = 0; i < wallpapersList.length; i++) {
|
|
combinedItems.push({
|
|
"path": wallpapersList[i],
|
|
"name": wallpapersList[i].split('/').pop(),
|
|
"isDirectory": false
|
|
});
|
|
}
|
|
|
|
combinedItems = sortFavoritesToTop(combinedItems);
|
|
|
|
// Apply filter if text is present
|
|
if (!panelContent.filterText || panelContent.filterText.trim().length === 0) {
|
|
filteredItems = combinedItems;
|
|
if (!skipSync)
|
|
syncModel();
|
|
return;
|
|
}
|
|
|
|
const results = FuzzySort.go(panelContent.filterText.trim(), combinedItems, {
|
|
"key": 'name',
|
|
"limit": 200
|
|
});
|
|
var filtered = results.map(function (r) {
|
|
return r.obj;
|
|
});
|
|
filteredItems = sortFavoritesToTop(filtered);
|
|
if (!skipSync)
|
|
syncModel();
|
|
}
|
|
|
|
// Copy filteredItems into the ListModel (full rebuild, no animation).
|
|
function syncModel() {
|
|
wallpaperModel.clear();
|
|
for (var i = 0; i < filteredItems.length; i++) {
|
|
wallpaperModel.append(filteredItems[i]);
|
|
}
|
|
wallpaperGridView.currentIndex = -1;
|
|
wallpaperGridView.positionViewAtBeginning();
|
|
}
|
|
|
|
// Animate a single item moving to its new position after a favorite toggle.
|
|
function handleFavoriteMove(path) {
|
|
// Find where the item currently sits in the model
|
|
var fromIndex = -1;
|
|
for (var i = 0; i < wallpaperModel.count; i++) {
|
|
if (wallpaperModel.get(i).path === path) {
|
|
fromIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
if (fromIndex === -1)
|
|
return;
|
|
|
|
// Find where it should be in the freshly-computed filteredItems
|
|
var toIndex = -1;
|
|
for (var j = 0; j < filteredItems.length; j++) {
|
|
if (filteredItems[j].path === path) {
|
|
toIndex = j;
|
|
break;
|
|
}
|
|
}
|
|
if (toIndex === -1 || fromIndex === toIndex)
|
|
return;
|
|
|
|
wallpaperGridView.animateMovement = true;
|
|
wallpaperModel.move(fromIndex, toIndex, 1);
|
|
animateMovementResetTimer.restart();
|
|
}
|
|
|
|
// Turn off move animations shortly after the move completes so that
|
|
// non-favorite model rebuilds (sort, filter, navigation) don't animate.
|
|
Timer {
|
|
id: animateMovementResetTimer
|
|
readonly property int settleDelay: 50
|
|
interval: Style.animationNormal + settleDelay
|
|
onTriggered: {
|
|
wallpaperGridView.animateMovement = false;
|
|
reconcileModel();
|
|
}
|
|
}
|
|
|
|
// Reorder wallpaperModel to match filteredItems using in-place moves.
|
|
// Avoids clear+rebuild which would destroy delegates and flash thumbnails.
|
|
function reconcileModel() {
|
|
for (var i = 0; i < filteredItems.length; i++) {
|
|
var currentPos = -1;
|
|
for (var j = i; j < wallpaperModel.count; j++) {
|
|
if (wallpaperModel.get(j).path === filteredItems[i].path) {
|
|
currentPos = j;
|
|
break;
|
|
}
|
|
}
|
|
if (currentPos !== -1 && currentPos !== i) {
|
|
wallpaperModel.move(currentPos, i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
refreshWallpaperScreenData();
|
|
}
|
|
|
|
Connections {
|
|
target: WallpaperService
|
|
function onWallpaperChanged(screenName, path) {
|
|
if (targetScreen !== null && screenName === targetScreen.name) {
|
|
currentWallpaper = WallpaperService.getWallpaperPathForSlot(targetScreen.name, WallpaperService.wallpaperSelectionAppearance);
|
|
}
|
|
}
|
|
function onWallpaperSelectionAppearanceChanged() {
|
|
if (targetScreen !== null) {
|
|
currentWallpaper = WallpaperService.getWallpaperPathForSlot(targetScreen.name, WallpaperService.wallpaperSelectionAppearance);
|
|
updateFiltered(false);
|
|
}
|
|
}
|
|
function onWallpaperDirectoryChanged(screenName, directory) {
|
|
if (targetScreen !== null && screenName === targetScreen.name) {
|
|
// Reset browse path when root directory changes
|
|
if (isBrowseMode) {
|
|
WallpaperService.navigateToRoot(targetScreen.name);
|
|
}
|
|
refreshWallpaperScreenData();
|
|
}
|
|
}
|
|
function onWallpaperListChanged(screenName, count) {
|
|
if (targetScreen !== null && screenName === targetScreen.name) {
|
|
refreshWallpaperScreenData();
|
|
}
|
|
}
|
|
function onBrowsePathChanged(screenName, path) {
|
|
if (targetScreen !== null && screenName === targetScreen.name) {
|
|
currentBrowsePath = path;
|
|
refreshWallpaperScreenData();
|
|
}
|
|
}
|
|
function onFavoritesChanged(path) {
|
|
updateFiltered(true); // recompute filteredItems but skip full model rebuild
|
|
handleFavoriteMove(path); // animate the item to its new position
|
|
}
|
|
}
|
|
|
|
function refreshWallpaperScreenData() {
|
|
if (targetScreen === null) {
|
|
return;
|
|
}
|
|
|
|
currentWallpaper = WallpaperService.getWallpaperPathForSlot(targetScreen.name, WallpaperService.wallpaperSelectionAppearance);
|
|
|
|
if (isBrowseMode) {
|
|
// In browse mode, scan current directory for both files and directories
|
|
var browsePath = WallpaperService.getCurrentBrowsePath(targetScreen.name);
|
|
currentBrowsePath = browsePath;
|
|
|
|
// Bump generation so stale scan callbacks from rapid navigation are ignored
|
|
var gen = ++_browseScanGeneration;
|
|
WallpaperService.scanDirectoryWithDirs(targetScreen.name, browsePath, function (result) {
|
|
if (gen !== _browseScanGeneration)
|
|
return; // Stale callback from a superseded navigation
|
|
wallpapersList = result.files;
|
|
directoriesList = result.directories;
|
|
Logger.d("WallpaperPanel", "Browse mode: Got", wallpapersList.length, "files and", directoriesList.length, "directories for screen", targetScreen.name);
|
|
updateFiltered();
|
|
});
|
|
} else {
|
|
// Normal mode: just use the wallpaper list from service
|
|
wallpapersList = WallpaperService.getWallpapersList(targetScreen.name);
|
|
directoriesList = [];
|
|
Logger.d("WallpaperPanel", "Got", wallpapersList.length, "wallpapers for screen", targetScreen.name);
|
|
updateFiltered();
|
|
}
|
|
}
|
|
|
|
function selectItem(path, isDirectory) {
|
|
if (isDirectory) {
|
|
WallpaperService.setBrowsePath(targetScreen.name, path);
|
|
} else {
|
|
var screen = Settings.data.wallpaper.setWallpaperOnAllMonitors ? undefined : targetScreen.name;
|
|
WallpaperService.changeWallpaper(path, screen, WallpaperService.wallpaperSelectionAppearance);
|
|
WallpaperService.applyFavoriteTheme(path, screen, WallpaperService.wallpaperSelectionAppearance);
|
|
}
|
|
}
|
|
|
|
// Helper function to cycle view modes
|
|
function cycleViewMode() {
|
|
var mode = Settings.data.wallpaper.viewMode;
|
|
if (mode === "single") {
|
|
Settings.data.wallpaper.viewMode = "recursive";
|
|
} else if (mode === "recursive") {
|
|
Settings.data.wallpaper.viewMode = "browse";
|
|
} else {
|
|
Settings.data.wallpaper.viewMode = "single";
|
|
}
|
|
}
|
|
|
|
// Helper function to get icon for current view mode
|
|
function getViewModeIcon() {
|
|
var mode = Settings.data.wallpaper.viewMode;
|
|
if (mode === "single")
|
|
return "folder";
|
|
if (mode === "recursive")
|
|
return "folders";
|
|
return "folder-open";
|
|
}
|
|
|
|
// Helper function to get tooltip for current view mode
|
|
function getViewModeTooltip() {
|
|
var mode = Settings.data.wallpaper.viewMode;
|
|
var modeName;
|
|
if (mode === "single")
|
|
modeName = I18n.tr("panels.wallpaper.view-mode-single");
|
|
else if (mode === "recursive")
|
|
modeName = I18n.tr("panels.wallpaper.view-mode-recursive");
|
|
else
|
|
modeName = I18n.tr("panels.wallpaper.view-mode-browse");
|
|
return I18n.tr("panels.wallpaper.view-mode-cycle-tooltip").replace("{mode}", modeName);
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: Style.marginM
|
|
|
|
// Combined toolbar: navigation (left) + actions (right)
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.marginS
|
|
|
|
// Left side: navigation (back, home, path)
|
|
NIconButton {
|
|
icon: "arrow-left"
|
|
tooltipText: I18n.tr("wallpaper.browse.go-up")
|
|
enabled: isBrowseMode && currentBrowsePath !== WallpaperService.getMonitorDirectory(targetScreen?.name ?? "")
|
|
onClicked: WallpaperService.navigateUp(targetScreen?.name ?? "")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
}
|
|
|
|
NIconButton {
|
|
icon: "home"
|
|
tooltipText: I18n.tr("wallpaper.browse.go-root")
|
|
enabled: isBrowseMode && currentBrowsePath !== WallpaperService.getMonitorDirectory(targetScreen?.name ?? "")
|
|
onClicked: WallpaperService.navigateToRoot(targetScreen?.name ?? "")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
}
|
|
|
|
NScrollText {
|
|
text: isBrowseMode ? currentBrowsePath : WallpaperService.getMonitorDirectory(targetScreen?.name ?? "")
|
|
Layout.fillWidth: true
|
|
scrollMode: NScrollText.ScrollMode.Hover
|
|
fadeCornerRadius: Style.radiusM
|
|
|
|
NText {
|
|
text: isBrowseMode ? currentBrowsePath : WallpaperService.getMonitorDirectory(targetScreen?.name ?? "")
|
|
pointSize: Style.fontSizeS
|
|
color: Color.mOnSurfaceVariant
|
|
}
|
|
}
|
|
|
|
// Right side: actions (view mode, hide filenames, refresh)
|
|
NIconButton {
|
|
property string sortOrder: Settings.data.wallpaper.sortOrder || "name"
|
|
icon: {
|
|
if (sortOrder === "date_desc")
|
|
return "clock";
|
|
if (sortOrder === "date_asc")
|
|
return "history";
|
|
if (sortOrder === "name_desc")
|
|
return "sort-descending";
|
|
if (sortOrder === "random")
|
|
return "arrows-shuffle";
|
|
return "sort-ascending";
|
|
}
|
|
tooltipText: {
|
|
if (sortOrder === "date_desc")
|
|
return I18n.tr("wallpaper.panel.sort-date-desc");
|
|
if (sortOrder === "date_asc")
|
|
return I18n.tr("wallpaper.panel.sort-date-asc");
|
|
if (sortOrder === "name_desc")
|
|
return I18n.tr("wallpaper.panel.sort-name-desc");
|
|
if (sortOrder === "random")
|
|
return I18n.tr("wallpaper.panel.sort-random");
|
|
return I18n.tr("wallpaper.panel.sort-name-asc");
|
|
}
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
onClicked: {
|
|
var next = "name";
|
|
if (sortOrder === "name")
|
|
next = "date_desc";
|
|
else if (sortOrder === "date_desc")
|
|
next = "name"; // Toggle simpler: Name -> Newest -> Name
|
|
// Expanded cycle: Name -> Newest -> Oldest -> Z-A -> Random -> Name
|
|
// User just asked for "newest first", so let's make it easy to reach.
|
|
// Let's do: Name (A-Z) -> Newest -> Oldest -> Name (Z-A) -> ...
|
|
|
|
if (sortOrder === "name")
|
|
next = "date_desc";
|
|
else if (sortOrder === "date_desc")
|
|
next = "date_asc";
|
|
else if (sortOrder === "date_asc")
|
|
next = "name_desc";
|
|
else if (sortOrder === "name_desc")
|
|
next = "random";
|
|
else
|
|
next = "name";
|
|
|
|
Settings.data.wallpaper.sortOrder = next;
|
|
}
|
|
}
|
|
|
|
NIconButton {
|
|
icon: getViewModeIcon()
|
|
tooltipText: getViewModeTooltip()
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
onClicked: cycleViewMode()
|
|
}
|
|
|
|
NIconButton {
|
|
icon: Settings.data.wallpaper.hideWallpaperFilenames ? "id-off" : "id"
|
|
tooltipText: Settings.data.wallpaper.hideWallpaperFilenames ? I18n.tr("panels.wallpaper.settings-hide-wallpaper-filenames-tooltip-show") : I18n.tr("panels.wallpaper.settings-hide-wallpaper-filenames-tooltip-hide")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
onClicked: Settings.data.wallpaper.hideWallpaperFilenames = !Settings.data.wallpaper.hideWallpaperFilenames
|
|
}
|
|
|
|
NIconButton {
|
|
icon: Settings.data.wallpaper.showHiddenFiles ? "eye" : "eye-closed"
|
|
tooltipText: Settings.data.wallpaper.showHiddenFiles ? I18n.tr("panels.wallpaper.settings-show-hidden-files-tooltip-hide") : I18n.tr("panels.wallpaper.settings-show-hidden-files-tooltip-show")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
onClicked: Settings.data.wallpaper.showHiddenFiles = !Settings.data.wallpaper.showHiddenFiles
|
|
}
|
|
|
|
NIconButton {
|
|
icon: "refresh"
|
|
tooltipText: I18n.tr("tooltips.refresh-wallpaper-list")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
onClicked: {
|
|
if (isBrowseMode) {
|
|
refreshWallpaperScreenData();
|
|
} else {
|
|
WallpaperService.refreshWallpapersList();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NGridView {
|
|
id: wallpaperGridView
|
|
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
visible: !WallpaperService.scanning
|
|
interactive: true
|
|
keyNavigationEnabled: true
|
|
keyNavigationWraps: false
|
|
highlightFollowsCurrentItem: false
|
|
currentIndex: -1
|
|
|
|
model: wallpaperModel
|
|
|
|
Component.onCompleted: {
|
|
positionViewAtBeginning();
|
|
}
|
|
|
|
property int columns: (screen.width > 1920) ? 5 : 4
|
|
property int itemSize: cellWidth
|
|
|
|
cellWidth: Math.floor((availableWidth - leftMargin - rightMargin) / columns)
|
|
cellHeight: Math.floor(itemSize * 0.7) + Style.marginXS + Style.fontSizeXS + Style.marginM
|
|
|
|
leftMargin: Style.marginS
|
|
rightMargin: Style.marginS
|
|
topMargin: Style.marginS
|
|
bottomMargin: Style.marginS
|
|
|
|
onCurrentIndexChanged: {
|
|
if (currentIndex >= 0) {
|
|
positionViewAtIndex(currentIndex, GridView.Contain);
|
|
}
|
|
}
|
|
|
|
onKeyPressed: event => {
|
|
if (Keybinds.checkKey(event, 'enter', Settings)) {
|
|
if (currentIndex >= 0 && currentIndex < filteredItems.length) {
|
|
selectItem(filteredItems[currentIndex]);
|
|
}
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
|
|
delegate: Item {
|
|
id: wallpaperItemWrapper
|
|
width: wallpaperGridView.cellWidth
|
|
height: wallpaperGridView.cellHeight
|
|
|
|
ColumnLayout {
|
|
id: wallpaperItem
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginXS
|
|
|
|
property string wallpaperPath: model.path ?? ""
|
|
property bool isDirectory: model.isDirectory ?? false
|
|
property bool isSelected: !isDirectory && (wallpaperPath === currentWallpaper)
|
|
property bool isFavorited: !isDirectory && WallpaperService.isFavorite(wallpaperPath, WallpaperService.wallpaperSelectionAppearance)
|
|
property string filename: model.name ?? wallpaperPath.split('/').pop()
|
|
property string cachedPath: ""
|
|
|
|
spacing: Style.marginXS
|
|
|
|
Component.onCompleted: {
|
|
if (!isDirectory && ImageCacheService.initialized) {
|
|
ImageCacheService.getThumbnail(wallpaperPath, function (path, success) {
|
|
if (wallpaperItem)
|
|
wallpaperItem.cachedPath = success ? path : wallpaperPath;
|
|
});
|
|
} else if (!isDirectory) {
|
|
cachedPath = wallpaperPath;
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: imageContainer
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
property real imageHeight: Math.round(wallpaperGridView.itemSize * 0.67)
|
|
|
|
// Directory display
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
height: imageContainer.imageHeight
|
|
color: Color.mSurfaceVariant
|
|
radius: Style.radiusM
|
|
visible: wallpaperItem.isDirectory
|
|
border.color: wallpaperGridView.currentIndex === index ? Color.mHover : Color.mSurface
|
|
border.width: Math.max(1, Style.borderL * 1.5)
|
|
|
|
ColumnLayout {
|
|
anchors.centerIn: parent
|
|
spacing: Style.marginS
|
|
|
|
NIcon {
|
|
icon: "folder"
|
|
pointSize: Style.fontSizeXXXL
|
|
color: Color.mPrimary
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
// Image display (for non-directories)
|
|
NImageRounded {
|
|
id: img
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
height: imageContainer.imageHeight
|
|
visible: !wallpaperItem.isDirectory
|
|
imagePath: wallpaperItem.cachedPath
|
|
radius: Style.radiusM
|
|
borderColor: {
|
|
if (wallpaperItem.isSelected) {
|
|
return Color.mSecondary;
|
|
}
|
|
if (wallpaperGridView.currentIndex === index) {
|
|
return Color.mHover;
|
|
}
|
|
return Color.mSurface;
|
|
}
|
|
borderWidth: Math.max(1, Style.borderL * 1.5)
|
|
imageFillMode: Image.PreserveAspectCrop
|
|
}
|
|
|
|
// Loading/error state background (for non-directories)
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
height: imageContainer.imageHeight
|
|
color: Color.mSurfaceVariant
|
|
radius: Style.radiusM
|
|
visible: !wallpaperItem.isDirectory && (img.status === Image.Loading || img.status === Image.Error || wallpaperItem.cachedPath === "")
|
|
|
|
NIcon {
|
|
icon: "image"
|
|
pointSize: Style.fontSizeL
|
|
color: Color.mOnSurfaceVariant
|
|
anchors.centerIn: parent
|
|
}
|
|
}
|
|
|
|
NBusyIndicator {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
y: (imageContainer.imageHeight - height) / 2
|
|
visible: !wallpaperItem.isDirectory && (img.status === Image.Loading || wallpaperItem.cachedPath === "")
|
|
running: visible
|
|
size: 18
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.top: parent.top
|
|
anchors.right: parent.right
|
|
anchors.margins: Style.marginS
|
|
width: 28
|
|
height: 28
|
|
radius: width / 2
|
|
color: Color.mSecondary
|
|
border.color: Color.mOutline
|
|
border.width: Style.borderS
|
|
visible: wallpaperItem.isSelected
|
|
|
|
NIcon {
|
|
icon: "check"
|
|
pointSize: Style.fontSizeM
|
|
color: Color.mOnSecondary
|
|
anchors.centerIn: parent
|
|
}
|
|
}
|
|
|
|
// Favorite star button (top-left)
|
|
Rectangle {
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.margins: Style.marginS
|
|
width: 28
|
|
height: 28
|
|
radius: width / 2
|
|
visible: !wallpaperItem.isDirectory && (wallpaperItem.isFavorited || hoverHandler.hovered || wallpaperGridView.currentIndex === index)
|
|
color: {
|
|
if (wallpaperItem.isFavorited)
|
|
return starHoverHandler.hovered ? Color.mHover : Color.mPrimary;
|
|
return starHoverHandler.hovered ? Color.mSurfaceVariant : Color.mSurface;
|
|
}
|
|
opacity: wallpaperItem.isFavorited || starHoverHandler.hovered ? 1.0 : 0.7
|
|
z: 5
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
}
|
|
}
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Style.animationFast
|
|
}
|
|
}
|
|
|
|
NIcon {
|
|
icon: wallpaperItem.isFavorited ? "star-filled" : "star"
|
|
pointSize: Style.fontSizeM
|
|
color: {
|
|
if (wallpaperItem.isFavorited)
|
|
return starHoverHandler.hovered ? Color.mOnHover : Color.mOnPrimary;
|
|
return starHoverHandler.hovered ? Color.mOnSurface : Color.mOnSurfaceVariant;
|
|
}
|
|
anchors.centerIn: parent
|
|
}
|
|
|
|
HoverHandler {
|
|
id: starHoverHandler
|
|
}
|
|
|
|
TapHandler {
|
|
onTapped: {
|
|
WallpaperService.toggleFavorite(wallpaperItem.wallpaperPath, WallpaperService.wallpaperSelectionAppearance);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Palette color dots (bottom-center, favorites only)
|
|
Row {
|
|
id: paletteRow
|
|
anchors.bottom: img.bottom
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.bottomMargin: Style.marginS
|
|
spacing: Style.marginXS
|
|
z: 5
|
|
visible: wallpaperItem.isFavorited && paletteRow.colors.length > 0
|
|
|
|
property int diameter: 25 * Style.uiScaleRatio
|
|
property int _favRevision: 0
|
|
property var favData: {
|
|
_favRevision;
|
|
WallpaperService.wallpaperSelectionAppearance;
|
|
return WallpaperService.getFavorite(wallpaperItem.wallpaperPath, WallpaperService.wallpaperSelectionAppearance);
|
|
}
|
|
property var colors: favData && favData.paletteColors ? favData.paletteColors : []
|
|
property bool isDark: {
|
|
if (!favData) {
|
|
return false;
|
|
}
|
|
if (favData.appearance === "dark") {
|
|
return true;
|
|
}
|
|
if (favData.appearance === "light") {
|
|
return false;
|
|
}
|
|
return favData.darkMode === true;
|
|
}
|
|
|
|
Connections {
|
|
target: WallpaperService
|
|
function onFavoriteDataUpdated(path) {
|
|
if (path === wallpaperItem.wallpaperPath)
|
|
paletteRow._favRevision++;
|
|
}
|
|
}
|
|
|
|
// Dark/light mode indicator
|
|
Rectangle {
|
|
width: paletteRow.diameter
|
|
height: paletteRow.diameter
|
|
radius: width * 0.5
|
|
color: Color.mSurface
|
|
border.color: Color.mShadow
|
|
border.width: Style.borderS
|
|
|
|
NIcon {
|
|
icon: paletteRow.isDark ? "moon" : "sun"
|
|
pointSize: parent.width * 0.45
|
|
color: Color.mOnSurface
|
|
anchors.centerIn: parent
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
model: paletteRow.colors
|
|
|
|
Rectangle {
|
|
width: paletteRow.diameter
|
|
height: paletteRow.diameter
|
|
radius: width * 0.5
|
|
color: modelData
|
|
border.color: Color.mShadow
|
|
border.width: Style.borderS
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
height: imageContainer.imageHeight
|
|
color: Color.mSurface
|
|
radius: Style.radiusM
|
|
opacity: (hoverHandler.hovered || wallpaperItem.isSelected || wallpaperGridView.currentIndex === index) ? 0 : 0.3
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Style.animationFast
|
|
}
|
|
}
|
|
}
|
|
|
|
HoverHandler {
|
|
id: hoverHandler
|
|
}
|
|
|
|
TapHandler {
|
|
onTapped: {
|
|
wallpaperGridView.forceActiveFocus();
|
|
wallpaperGridView.currentIndex = index;
|
|
selectItem(wallpaperItem.wallpaperPath, wallpaperItem.isDirectory);
|
|
}
|
|
}
|
|
}
|
|
|
|
NText {
|
|
text: wallpaperItem.filename
|
|
visible: !Settings.data.wallpaper.hideWallpaperFilenames
|
|
color: (hoverHandler.hovered || wallpaperItem.isSelected || wallpaperGridView.currentIndex === index) ? Color.mOnSurface : Color.mOnSurfaceVariant
|
|
pointSize: Style.fontSizeXS
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Style.marginS
|
|
Layout.rightMargin: Style.marginS
|
|
Layout.alignment: Qt.AlignHCenter
|
|
horizontalAlignment: Text.AlignHCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Empty / scanning state
|
|
Rectangle {
|
|
color: Color.mSurface
|
|
radius: Style.radiusM
|
|
border.color: Color.mOutline
|
|
border.width: Style.borderS
|
|
visible: (wallpaperModel.count === 0 && !WallpaperService.scanning) || WallpaperService.scanning
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 130
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
visible: WallpaperService.scanning
|
|
NBusyIndicator {
|
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
visible: wallpaperModel.count === 0 && !WallpaperService.scanning
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
NIcon {
|
|
icon: "folder-open"
|
|
pointSize: Style.fontSizeXXL
|
|
color: Color.mOnSurface
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
NText {
|
|
text: (panelContent.filterText && panelContent.filterText.length > 0) ? I18n.tr("wallpaper.no-match") : (isBrowseMode ? I18n.tr("wallpaper.browse.empty-directory") : I18n.tr("wallpaper.no-wallpaper"))
|
|
color: Color.mOnSurface
|
|
font.weight: Style.fontWeightBold
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
NText {
|
|
text: (panelContent.filterText && panelContent.filterText.length > 0) ? I18n.tr("wallpaper.try-different-search") : (isBrowseMode ? I18n.tr("wallpaper.browse.go-up-hint") : I18n.tr("wallpaper.configure-directory"))
|
|
color: Color.mOnSurfaceVariant
|
|
wrapMode: Text.WordWrap
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Component for Wallhaven wallpapers view
|
|
component WallhavenView: Item {
|
|
id: wallhavenViewRoot
|
|
property alias gridView: wallhavenGridView
|
|
property alias pageInput: pageInput
|
|
|
|
property var wallpapers: []
|
|
property bool loading: false
|
|
property string errorMessage: ""
|
|
property bool initialized: false
|
|
property bool searchScheduled: false
|
|
|
|
Connections {
|
|
target: typeof WallhavenService !== "undefined" ? WallhavenService : null
|
|
function onSearchCompleted(results, meta) {
|
|
wallhavenViewRoot.wallpapers = results || [];
|
|
wallhavenViewRoot.loading = false;
|
|
wallhavenViewRoot.errorMessage = "";
|
|
wallhavenViewRoot.searchScheduled = false;
|
|
}
|
|
function onSearchFailed(error) {
|
|
wallhavenViewRoot.loading = false;
|
|
wallhavenViewRoot.errorMessage = error || "";
|
|
wallhavenViewRoot.searchScheduled = false;
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
// Initialize service properties and perform initial search if Wallhaven is active
|
|
if (typeof WallhavenService !== "undefined" && Settings.data.wallpaper.useWallhaven && !initialized) {
|
|
// Set flags immediately to prevent race conditions
|
|
if (WallhavenService.initialSearchScheduled) {
|
|
// Another instance already scheduled the search, just initialize properties
|
|
initialized = true;
|
|
return;
|
|
}
|
|
|
|
// We're the first one - claim the search
|
|
initialized = true;
|
|
WallhavenService.initialSearchScheduled = true;
|
|
WallhavenService.categories = Settings.data.wallpaper.wallhavenCategories;
|
|
WallhavenService.purity = Settings.data.wallpaper.wallhavenPurity;
|
|
WallhavenService.sorting = Settings.data.wallpaper.wallhavenSorting;
|
|
WallhavenService.order = Settings.data.wallpaper.wallhavenOrder;
|
|
|
|
// Initialize resolution settings
|
|
var width = Settings.data.wallpaper.wallhavenResolutionWidth || "";
|
|
var height = Settings.data.wallpaper.wallhavenResolutionHeight || "";
|
|
var mode = Settings.data.wallpaper.wallhavenResolutionMode || "atleast";
|
|
if (width && height) {
|
|
var resolution = width + "x" + height;
|
|
if (mode === "atleast") {
|
|
WallhavenService.minResolution = resolution;
|
|
WallhavenService.resolutions = "";
|
|
} else {
|
|
WallhavenService.minResolution = "";
|
|
WallhavenService.resolutions = resolution;
|
|
}
|
|
} else {
|
|
WallhavenService.minResolution = "";
|
|
WallhavenService.resolutions = "";
|
|
}
|
|
|
|
// Now check if we can actually search (fetching check is in WallhavenService.search)
|
|
// Use persisted currentPage to maintain state across window reopening
|
|
loading = true;
|
|
WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", WallhavenService.currentPage);
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: Style.marginM
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
NGridView {
|
|
id: wallhavenGridView
|
|
|
|
anchors.fill: parent
|
|
|
|
visible: !loading && errorMessage === "" && (wallpapers && wallpapers.length > 0)
|
|
interactive: true
|
|
keyNavigationEnabled: true
|
|
keyNavigationWraps: false
|
|
highlightFollowsCurrentItem: false
|
|
currentIndex: -1
|
|
|
|
model: wallpapers || []
|
|
|
|
onModelChanged: {
|
|
// Reset selection and scroll position when model changes
|
|
currentIndex = -1;
|
|
positionViewAtBeginning();
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
positionViewAtBeginning();
|
|
}
|
|
|
|
property int columns: (screen.width > 1920) ? 5 : 4
|
|
property int itemSize: cellWidth
|
|
|
|
cellWidth: Math.floor((availableWidth - leftMargin - rightMargin) / columns)
|
|
cellHeight: Math.floor(itemSize * 0.7) + Style.marginXS + Style.fontSizeXS + Style.marginM
|
|
|
|
leftMargin: Style.marginS
|
|
rightMargin: Style.marginS
|
|
topMargin: Style.marginS
|
|
bottomMargin: Style.marginS
|
|
|
|
onCurrentIndexChanged: {
|
|
if (currentIndex >= 0) {
|
|
positionViewAtIndex(currentIndex, GridView.Contain);
|
|
}
|
|
}
|
|
|
|
onKeyPressed: event => {
|
|
if (Keybinds.checkKey(event, 'enter', Settings)) {
|
|
if (currentIndex >= 0 && currentIndex < wallpapers.length) {
|
|
let wallpaper = wallpapers[currentIndex];
|
|
wallhavenDownloadAndApply(wallpaper);
|
|
}
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
|
|
delegate: Item {
|
|
id: wallhavenItemWrapper
|
|
width: wallhavenGridView.cellWidth
|
|
height: wallhavenGridView.cellHeight
|
|
|
|
ColumnLayout {
|
|
id: wallhavenItem
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginXS
|
|
|
|
property string thumbnailUrl: (modelData && typeof WallhavenService !== "undefined") ? WallhavenService.getThumbnailUrl(modelData, "large") : ""
|
|
property string wallpaperId: (modelData && modelData.id) ? modelData.id : ""
|
|
|
|
spacing: Style.marginXS
|
|
|
|
Item {
|
|
id: imageContainer
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
property real imageHeight: Math.round(wallhavenGridView.itemSize * 0.67)
|
|
|
|
NImageRounded {
|
|
id: img
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
height: imageContainer.imageHeight
|
|
imagePath: wallhavenItem.thumbnailUrl
|
|
radius: Style.radiusM
|
|
borderColor: {
|
|
if (wallhavenGridView.currentIndex === index) {
|
|
return Color.mHover;
|
|
}
|
|
return Color.mSurface;
|
|
}
|
|
borderWidth: Math.max(1, Style.borderL * 1.5)
|
|
imageFillMode: Image.PreserveAspectCrop
|
|
}
|
|
|
|
// Loading/error state background
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
height: imageContainer.imageHeight
|
|
color: Color.mSurfaceVariant
|
|
radius: Style.radiusM
|
|
visible: img.status === Image.Loading || img.status === Image.Error || wallhavenItem.thumbnailUrl === ""
|
|
|
|
NIcon {
|
|
icon: "image"
|
|
pointSize: Style.fontSizeL
|
|
color: Color.mOnSurfaceVariant
|
|
anchors.centerIn: parent
|
|
}
|
|
}
|
|
|
|
NBusyIndicator {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
y: (imageContainer.imageHeight - height) / 2
|
|
visible: img.status === Image.Loading
|
|
running: visible
|
|
size: 18
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
height: imageContainer.imageHeight
|
|
color: Color.mSurface
|
|
radius: Style.radiusM
|
|
opacity: (hoverHandler.hovered || wallhavenGridView.currentIndex === index) ? 0 : 0.3
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Style.animationFast
|
|
}
|
|
}
|
|
}
|
|
|
|
HoverHandler {
|
|
id: hoverHandler
|
|
}
|
|
|
|
TapHandler {
|
|
onTapped: {
|
|
wallhavenGridView.forceActiveFocus();
|
|
wallhavenGridView.currentIndex = index;
|
|
wallhavenDownloadAndApply(modelData);
|
|
}
|
|
}
|
|
}
|
|
|
|
NText {
|
|
text: wallhavenItem.wallpaperId || I18n.tr("common.unknown")
|
|
visible: !Settings.data.wallpaper.hideWallpaperFilenames
|
|
color: (hoverHandler.hovered || wallhavenGridView.currentIndex === index) ? Color.mOnSurface : Color.mOnSurfaceVariant
|
|
pointSize: Style.fontSizeXS
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Style.marginS
|
|
Layout.rightMargin: Style.marginS
|
|
Layout.alignment: Qt.AlignHCenter
|
|
horizontalAlignment: Text.AlignHCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Loading overlay - fills same space as GridView to prevent jumping
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Color.mSurface
|
|
radius: Style.radiusM
|
|
border.color: Color.mOutline
|
|
border.width: Style.borderS
|
|
visible: loading || (typeof WallhavenService !== "undefined" && WallhavenService.fetching)
|
|
z: 10
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginL
|
|
spacing: Style.marginM
|
|
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
|
|
NBusyIndicator {
|
|
size: Style.baseWidgetSize * 1.5
|
|
color: Color.mPrimary
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
|
|
NText {
|
|
text: I18n.tr("wallpaper.wallhaven.loading")
|
|
color: Color.mOnSurfaceVariant
|
|
pointSize: Style.fontSizeM
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Error overlay
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Color.mSurface
|
|
radius: Style.radiusM
|
|
border.color: Color.mOutline
|
|
border.width: Style.borderS
|
|
visible: errorMessage !== "" && !loading
|
|
z: 10
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginL
|
|
spacing: Style.marginM
|
|
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
|
|
NIcon {
|
|
icon: "alert-circle"
|
|
pointSize: Style.fontSizeXXL
|
|
color: Color.mError
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
|
|
NText {
|
|
text: errorMessage
|
|
color: Color.mOnSurface
|
|
wrapMode: Text.WordWrap
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.fillWidth: true
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Empty state overlay
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Color.mSurface
|
|
radius: Style.radiusM
|
|
border.color: Color.mOutline
|
|
border.width: Style.borderS
|
|
visible: (!wallpapers || wallpapers.length === 0) && !loading && errorMessage === ""
|
|
z: 10
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginL
|
|
spacing: Style.marginM
|
|
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
|
|
NIcon {
|
|
icon: "image"
|
|
pointSize: Style.fontSizeXXL
|
|
color: Color.mOnSurfaceVariant
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
|
|
NText {
|
|
text: I18n.tr("wallpaper.wallhaven.no-results")
|
|
color: Color.mOnSurface
|
|
wrapMode: Text.WordWrap
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.fillWidth: true
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pagination
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
visible: errorMessage === "" && typeof WallhavenService !== "undefined"
|
|
spacing: Style.marginS
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
NIconButton {
|
|
icon: "chevron-left"
|
|
enabled: !loading && WallhavenService.currentPage > 1 && !WallhavenService.fetching
|
|
onClicked: WallhavenService.previousPage()
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: Style.marginXS
|
|
|
|
NText {
|
|
text: I18n.tr("wallpaper.wallhaven.page-prefix")
|
|
color: Color.mOnSurface
|
|
}
|
|
|
|
NTextInput {
|
|
id: pageInput
|
|
text: "" + WallhavenService.currentPage
|
|
Layout.preferredWidth: 50 * Style.uiScaleRatio
|
|
Layout.maximumWidth: 50 * Style.uiScaleRatio
|
|
Layout.fillWidth: false
|
|
minimumInputWidth: 50 * Style.uiScaleRatio
|
|
horizontalAlignment: Text.AlignHCenter
|
|
inputMethodHints: Qt.ImhDigitsOnly
|
|
enabled: !loading && !WallhavenService.fetching
|
|
showClearButton: false
|
|
|
|
Connections {
|
|
target: WallhavenService
|
|
function onCurrentPageChanged() {
|
|
pageInput.text = "" + WallhavenService.currentPage;
|
|
}
|
|
}
|
|
|
|
function submitPage() {
|
|
var page = parseInt(text);
|
|
if (!isNaN(page) && page >= 1 && page <= WallhavenService.lastPage) {
|
|
if (page !== WallhavenService.currentPage) {
|
|
WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", page);
|
|
}
|
|
} else {
|
|
// Reset to current page if invalid
|
|
text = "" + WallhavenService.currentPage;
|
|
}
|
|
// Force focus loss to ensure UI updates cleanly
|
|
pageInput.inputItem.focus = false;
|
|
}
|
|
|
|
onEditingFinished: submitPage()
|
|
}
|
|
|
|
NText {
|
|
text: I18n.tr("wallpaper.wallhaven.page-suffix").replace("{total}", WallhavenService.lastPage)
|
|
color: Color.mOnSurface
|
|
}
|
|
}
|
|
|
|
NIconButton {
|
|
icon: "chevron-right"
|
|
enabled: WallhavenService.currentPage < WallhavenService.lastPage && !WallhavenService.fetching
|
|
onClicked: WallhavenService.nextPage()
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------
|
|
function wallhavenDownloadAndApply(wallpaper, targetScreen) {
|
|
if (typeof WallhavenService !== "undefined") {
|
|
WallhavenService.downloadWallpaper(wallpaper, function (success, localPath) {
|
|
if (success) {
|
|
var whScreen = Settings.data.wallpaper.setWallpaperOnAllMonitors ? undefined : Quickshell.screens[currentScreenIndex].name;
|
|
if (!Settings.data.wallpaper.setWallpaperOnAllMonitors && currentScreenIndex < Quickshell.screens.length) {
|
|
WallpaperService.changeWallpaper(localPath, Quickshell.screens[currentScreenIndex].name, WallpaperService.wallpaperSelectionAppearance);
|
|
} else {
|
|
WallpaperService.changeWallpaper(localPath, undefined, WallpaperService.wallpaperSelectionAppearance);
|
|
}
|
|
WallpaperService.applyFavoriteTheme(localPath, whScreen, WallpaperService.wallpaperSelectionAppearance);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|