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: 650 * Style.uiScaleRatio preferredWidthRatio: 0.5 preferredHeightRatio: 0.5 // 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 readonly property bool headerScreensStripAvailable: !Settings.data.wallpaper.setWallpaperOnAllMonitors || Settings.data.wallpaper.enableMultiMonitorDirectories readonly property bool headerDevicesButtonVisible: Quickshell.screens.length > 1 || Settings.data.wallpaper.enableMultiMonitorDirectories 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 { visible: Settings.data.wallpaper.enabled icon: "sun" tooltipText: Settings.data.wallpaper.linkLightAndDarkWallpapers ? I18n.tr("wallpaper.panel.header-sun-linked-tooltip") : I18n.tr("wallpaper.panel.header-sun-separate-tooltip") baseSize: Style.baseWidgetSize * 0.8 colorBg: !Settings.data.wallpaper.linkLightAndDarkWallpapers ? Color.mPrimary : Color.smartAlpha(Color.mSurfaceVariant) colorFg: !Settings.data.wallpaper.linkLightAndDarkWallpapers ? Color.mOnPrimary : Color.mPrimary onClicked: Settings.data.wallpaper.linkLightAndDarkWallpapers = !Settings.data.wallpaper.linkLightAndDarkWallpapers } NIconButton { visible: Settings.data.wallpaper.enabled && panelContent.headerDevicesButtonVisible icon: "devices" tooltipText: Settings.data.wallpaper.setWallpaperOnAllMonitors ? I18n.tr("wallpaper.panel.header-devices-apply-all-tooltip") : I18n.tr("wallpaper.panel.header-devices-per-monitor-tooltip") baseSize: Style.baseWidgetSize * 0.8 colorBg: !Settings.data.wallpaper.setWallpaperOnAllMonitors ? Color.mPrimary : Color.smartAlpha(Color.mSurfaceVariant) colorFg: !Settings.data.wallpaper.setWallpaperOnAllMonitors ? Color.mOnPrimary : Color.mPrimary onClicked: Settings.data.wallpaper.setWallpaperOnAllMonitors = !Settings.data.wallpaper.setWallpaperOnAllMonitors } 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 } NTabBar { id: appearanceTabBar visible: Settings.data.wallpaper.enabled && !Settings.data.wallpaper.linkLightAndDarkWallpapers 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 } } NTabBar { id: screenTabBar visible: panelContent.headerScreensStripAvailable 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 { id: wallpaperScreenView property var targetScreen property alias gridView: wallpaperGridView // Local reactive state for this screen property list 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 // Favorited paths (any light/dark) first, then the rest function sortFavoritesToTop(items) { var favs = []; var rest = []; for (var i = 0; i < items.length; i++) { var it = items[i]; if (!it.isDirectory && WallpaperService.isFavorite(it.path)) { favs.push(it); } else { rest.push(it); } } return favs.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: { WallpaperService.favoritesRevision; return !isDirectory && WallpaperService.isFavorite(wallpaperPath); } 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 z: 6 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 } MouseArea { anchors.fill: parent onClicked: {} } } // 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: 11 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: { var mon = Settings.data.wallpaper.setWallpaperOnAllMonitors ? undefined : (wallpaperScreenView.targetScreen ? wallpaperScreenView.targetScreen.name : undefined); WallpaperService.toggleFavorite(wallpaperItem.wallpaperPath, WallpaperService.wallpaperSelectionAppearance, mon); } } } // Palette color dots (bottom-center, favorites only) — taps must not fall through to selectItem Item { id: paletteRow anchors.bottom: img.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.bottomMargin: Style.marginS z: 10 implicitWidth: paletteRowRow.implicitWidth implicitHeight: paletteRowRow.implicitHeight width: implicitWidth height: implicitHeight visible: wallpaperItem.isFavorited && paletteRow.colors.length > 0 property int diameter: 25 * Style.uiScaleRatio property int _favRevision: 0 property var favData: { _favRevision; WallpaperService.favoritesRevision; Settings.data.wallpaper.linkLightAndDarkWallpapers; return WallpaperService.getFavoriteForDisplay(wallpaperItem.wallpaperPath); } 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++; } } Row { id: paletteRowRow spacing: Style.marginXS // Sun/moon only when light/dark share one wallpaper (which appearance the favorite targets) Rectangle { width: paletteRow.diameter height: paletteRow.diameter radius: width * 0.5 visible: Settings.data.wallpaper.linkLightAndDarkWallpapers 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 } } } TapHandler { onTapped: {} } } 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); } }); } } } }