From 74ba883dd8b4ae8a9a95a4cd32b78d74d1780b42 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 22 Nov 2025 13:51:58 +0100 Subject: [PATCH] initial commit --- Assets/settings-default.json | 1 - Commons/Settings.qml | 2 - Commons/ShellState.qml | 183 ++++++++++++++++++ Modules/LockScreen/LockScreen.qml | 19 +- .../Tabs/ColorScheme/ColorSchemeTab.qml | 18 -- .../Tabs/ColorScheme/SchemeDownloader.qml | 182 +++++++++-------- .../Panels/Settings/Tabs/LockScreenTab.qml | 7 - Services/Compositor/CompositorService.qml | 90 ++++++--- Services/Noctalia/UpdateService.qml | 106 +++++----- Services/System/NotificationService.qml | 82 +++++--- Services/System/ProgramCheckerService.qml | 4 +- Services/Theming/TemplateRegistry.qml | 11 -- Services/UI/WallpaperService.qml | 62 +++--- shell.qml | 13 +- 14 files changed, 515 insertions(+), 265 deletions(-) create mode 100644 Commons/ShellState.qml diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 68e12d93..79645fa0 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -70,7 +70,6 @@ "animationDisabled": false, "compactLockScreen": false, "lockOnSuspend": true, - "showHibernateOnLockScreen": true, "enableShadows": true, "shadowDirection": "bottom_right", "shadowOffsetX": 2, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index a35f0de3..2c03167c 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -215,7 +215,6 @@ Singleton { property bool animationDisabled: false property bool compactLockScreen: false property bool lockOnSuspend: true - property bool showHibernateOnLockScreen: true property bool enableShadows: true property string shadowDirection: "bottom_right" property int shadowOffsetX: 2 @@ -507,7 +506,6 @@ Singleton { property bool walker: false property bool code: false property bool spicetify: false - property bool telegram: false property bool enableUserTemplates: false } diff --git a/Commons/ShellState.qml b/Commons/ShellState.qml new file mode 100644 index 00000000..8fda49ca --- /dev/null +++ b/Commons/ShellState.qml @@ -0,0 +1,183 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// Centralized shell state management for small cache files +Singleton { + id: root + + property string stateFile: "" + property bool isLoaded: false + + // State properties for different services + readonly property alias data: adapter + + // Signals for state changes + signal displayStateChanged + signal notificationsStateChanged + signal changelogStateChanged + signal colorSchemesListChanged + + Component.onCompleted: { + // Setup state file path (needs Settings to be available) + Qt.callLater(() => { + if (typeof Settings !== 'undefined' && Settings.cacheDir) { + stateFile = Settings.cacheDir + "shell-state.json"; + stateFileView.path = stateFile; + } + }); + } + + // FileView for shell state + FileView { + id: stateFileView + printErrors: false + watchChanges: false + + adapter: JsonAdapter { + id: adapter + + // CompositorService: display scales + property var display: ({}) + + // NotificationService: notification state + property var notificationsState: ({ + lastSeenTs: 0 + }) + + // UpdateService: changelog state + property var changelogState: ({ + lastSeenVersion: "" + }) + + // SchemeDownloader: color schemes list + property var colorSchemesList: ({ + schemes: [], + timestamp: 0 + }) + + // WallpaperService: current wallpapers per screen + property var wallpapers: ({}) + } + + onLoaded: { + root.isLoaded = true; + Logger.d("ShellState", "Loaded state file"); + } + + onLoadFailed: error => { + if (error === 2) { + // File doesn't exist, will be created on first write + root.isLoaded = true; + Logger.d("ShellState", "State file doesn't exist, will create on first write"); + } else { + Logger.e("ShellState", "Failed to load state file:", error); + root.isLoaded = true; + } + } + } + + // Debounced save timer + Timer { + id: saveTimer + interval: 300 + onTriggered: performSave() + } + + property bool saveQueued: false + + function save() { + saveQueued = true; + saveTimer.restart(); + } + + function performSave() { + if (!saveQueued || !stateFile) { + return; + } + + saveQueued = false; + + try { + // Ensure cache directory exists + Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); + + Qt.callLater(() => { + try { + stateFileView.writeAdapter(); + Logger.d("ShellState", "Saved state file"); + } catch (writeError) { + Logger.e("ShellState", "Failed to write state file:", writeError); + } + }); + } catch (error) { + Logger.e("ShellState", "Failed to save state:", error); + } + } + + // Convenience functions for each service + + // Display state (CompositorService) + function setDisplay(displayData) { + adapter.display = displayData; + save(); + displayStateChanged(); + } + + function getDisplay() { + return adapter.display || {}; + } + + // Notifications state (NotificationService) + function setNotificationsState(stateData) { + adapter.notificationsState = stateData; + save(); + notificationsStateChanged(); + } + + function getNotificationsState() { + return adapter.notificationsState || { + lastSeenTs: 0 + }; + } + + // Changelog state (UpdateService) + function setChangelogState(stateData) { + adapter.changelogState = stateData; + save(); + changelogStateChanged(); + } + + function getChangelogState() { + return adapter.changelogState || { + lastSeenVersion: "" + }; + } + + // Color schemes list (SchemeDownloader) + function setColorSchemesList(listData) { + adapter.colorSchemesList = listData; + save(); + colorSchemesListChanged(); + } + + function getColorSchemesList() { + return adapter.colorSchemesList || { + schemes: [], + timestamp: 0 + }; + } + + // Wallpapers (WallpaperService) + function setWallpapers(wallpapersData) { + adapter.wallpapers = wallpapersData; + save(); + } + + function getWallpapers() { + return adapter.wallpapers || {}; + } +} + diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 54ce8fad..ee4afe6b 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -343,15 +343,15 @@ Loader { text: { var lang = I18n.locale.name.split("_")[0]; var formats = { - "en": "dddd, MMMM d", "de": "dddd, d. MMMM", - "fr": "dddd d MMMM", "es": "dddd, d 'de' MMMM", + "fr": "dddd d MMMM", "pt": "dddd, d 'de' MMMM", "zh": "yyyy年M月d日 dddd", - "nl": "dddd d MMMM" + "uk": "dddd, d MMMM", + "tr": "dddd, d MMMM" }; - return I18n.locale.toString(Time.now, formats[lang] || "dddd, d MMMM"); + return I18n.locale.toString(Time.now, formats[lang] || "dddd, MMMM d"); } pointSize: Style.fontSizeXL font.weight: Font.Medium @@ -524,7 +524,7 @@ Loader { } Text { id: hibernateText - text: Settings.data.general.showHibernateOnLockScreen ? I18n.tr("session-menu.hibernate") : "" + text: I18n.tr("session-menu.hibernate") font.pointSize: buttonRowTextMeasurer.fontSize font.weight: Font.Medium } @@ -550,7 +550,7 @@ Loader { // Button row needs: margins + 5 buttons + 4 spacings + margins // Plus ColumnLayout margins (14 on each side = 28 total) // Add extra buffer to ensure password input has proper padding - property real minButtonRowWidth: buttonRowTextMeasurer.minButtonWidth > 0 ? ((Settings.data.general.showHibernateOnLockScreen ? 5 : 4) * buttonRowTextMeasurer.minButtonWidth) + 40 + (2 * Style.marginM) + 28 + (2 * Style.marginM) : 750 + property real minButtonRowWidth: buttonRowTextMeasurer.minButtonWidth > 0 ? (5 * buttonRowTextMeasurer.minButtonWidth) + 40 + (2 * Style.marginM) + 28 + (2 * Style.marginM) : 750 width: Math.max(750, minButtonRowWidth) ColumnLayout { @@ -749,7 +749,7 @@ Loader { } } - // Forecast + // 3-day forecast RowLayout { visible: Settings.data.location.weatherEnabled && LocationService.data.weather !== null Layout.preferredWidth: 260 @@ -757,7 +757,7 @@ Loader { spacing: 4 Repeater { - model: MediaService.currentPlayer && MediaService.canPlay ? 3 : 4 + model: 3 delegate: ColumnLayout { Layout.fillWidth: true spacing: 3 @@ -804,6 +804,8 @@ Loader { Item { Layout.fillWidth: true + visible: !(Settings.data.location.weatherEnabled && LocationService.data.weather !== null) + Layout.preferredWidth: visible ? 1 : 0 } // Battery and Keyboard Layout (full mode only) @@ -1181,7 +1183,6 @@ Loader { } Rectangle { - visible: Settings.data.general.showHibernateOnLockScreen Layout.fillWidth: true Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 diff --git a/Modules/Panels/Settings/Tabs/ColorScheme/ColorSchemeTab.qml b/Modules/Panels/Settings/Tabs/ColorScheme/ColorSchemeTab.qml index 124aa4c6..3fdf9609 100644 --- a/Modules/Panels/Settings/Tabs/ColorScheme/ColorSchemeTab.qml +++ b/Modules/Panels/Settings/Tabs/ColorScheme/ColorSchemeTab.qml @@ -835,24 +835,6 @@ ColumnLayout { } } } - - NCheckbox { - label: "Telegram" - description: ProgramCheckerService.telegramAvailable ? I18n.tr("settings.color-scheme.templates.programs.telegram.description", { - "filepath": "~/.config/telegram-desktop/themes/noctalia.tdesktop-theme" - }) : I18n.tr("settings.color-scheme.templates.programs.telegram.description-missing", { - "app": "telegram" - }) - checked: Settings.data.templates.telegram - enabled: ProgramCheckerService.telegramAvailable - opacity: ProgramCheckerService.telegramAvailable ? 1.0 : 0.6 - onToggled: checked => { - if (ProgramCheckerService.telegramAvailable) { - Settings.data.templates.telegram = checked; - AppThemeService.generate(); - } - } - } } // Miscellaneous NCollapsible { diff --git a/Modules/Panels/Settings/Tabs/ColorScheme/SchemeDownloader.qml b/Modules/Panels/Settings/Tabs/ColorScheme/SchemeDownloader.qml index 1c7b4016..a74760bf 100644 --- a/Modules/Panels/Settings/Tabs/ColorScheme/SchemeDownloader.qml +++ b/Modules/Panels/Settings/Tabs/ColorScheme/SchemeDownloader.qml @@ -26,8 +26,7 @@ Popup { property var schemeColorsCache: ({}) property int cacheVersion: 0 - // Cache for available schemes list - property string schemesCacheFile: Settings.cacheDir + "color-schemes-list.json" + // Cache for available schemes list (uses ShellState singleton) property int schemesCacheUpdateFrequency: 2 * 60 * 60 // 2 hours in seconds // Cache for repo branch info (to reduce API calls during downloads) @@ -99,33 +98,6 @@ Popup { xhr.send(); } - // Cache file for schemes list - FileView { - id: schemesCacheFileView - path: schemesCacheFile - printErrors: false - - JsonAdapter { - id: schemesCacheAdapter - property var schemes: [] - property real timestamp: 0 - } - - onLoaded: { - loadSchemesFromCache(); - } - - onLoadFailed: function (error) { - if (error.toString().includes("No such file") || error === 2) { - // Cache doesn't exist, fetch from API (only if popup is open) - if (root.visible) { - Qt.callLater(() => { - fetchAvailableSchemesFromAPI(); - }); - } - } - } - } background: Rectangle { color: Color.mSurface @@ -135,58 +107,96 @@ Popup { } function loadSchemesFromCache() { - const now = Time.timestamp; + try { + const now = Time.timestamp; + const cacheData = ShellState.getColorSchemesList(); + const cachedSchemes = cacheData.schemes || []; + const cachedTimestamp = cacheData.timestamp || 0; - // Check if cache is expired or missing - if (!schemesCacheAdapter.timestamp || (now >= schemesCacheAdapter.timestamp + schemesCacheUpdateFrequency)) { - // Only fetch from API if we haven't fetched recently (prevent rapid repeated calls) - const timeSinceLastFetch = now - lastApiFetchTime; - if (timeSinceLastFetch >= minApiFetchInterval) { - Logger.d("ColorSchemeDownload", "Cache expired or missing, fetching new schemes"); - fetchAvailableSchemesFromAPI(); - return; - } else { - // Use cached data even if expired, to avoid rate limits - Logger.d("ColorSchemeDownload", "Cache expired but recent API call detected, using cached data"); - if (schemesCacheAdapter.schemes && schemesCacheAdapter.schemes.length > 0) { - availableSchemes = schemesCacheAdapter.schemes; - hasInitialData = true; - fetching = false; + // Check if cache is expired or missing + if (!cachedTimestamp || (now >= cachedTimestamp + schemesCacheUpdateFrequency)) { + // Try migration first if cache is empty + if (cachedSchemes.length === 0) { + migrateFromOldSchemesList(); + } + + // Only fetch from API if we haven't fetched recently (prevent rapid repeated calls) + const timeSinceLastFetch = now - lastApiFetchTime; + if (timeSinceLastFetch >= minApiFetchInterval) { + Logger.d("ColorSchemeDownload", "Cache expired or missing, fetching new schemes"); + fetchAvailableSchemesFromAPI(); return; + } else { + // Use cached data even if expired, to avoid rate limits + Logger.d("ColorSchemeDownload", "Cache expired but recent API call detected, using cached data"); + if (cachedSchemes.length > 0) { + availableSchemes = cachedSchemes; + hasInitialData = true; + fetching = false; + return; + } } } - } - const ageMinutes = Math.round((now - schemesCacheAdapter.timestamp) / 60); - Logger.d("ColorSchemeDownload", "Loading cached schemes (age:", ageMinutes, "minutes)"); + const ageMinutes = Math.round((now - cachedTimestamp) / 60); + Logger.d("ColorSchemeDownload", "Loading cached schemes from ShellState (age:", ageMinutes, "minutes)"); - if (schemesCacheAdapter.schemes && schemesCacheAdapter.schemes.length > 0) { - availableSchemes = schemesCacheAdapter.schemes; - hasInitialData = true; - fetching = false; - } else { - // Cache is empty, only fetch if we haven't fetched recently - const timeSinceLastFetch = now - lastApiFetchTime; - if (timeSinceLastFetch >= minApiFetchInterval) { - fetchAvailableSchemesFromAPI(); - } else { - Logger.d("ColorSchemeDownload", "Cache empty but recent API call detected, skipping fetch"); + if (cachedSchemes.length > 0) { + availableSchemes = cachedSchemes; + hasInitialData = true; fetching = false; + } else { + // Cache is empty, only fetch if we haven't fetched recently + const timeSinceLastFetch = now - lastApiFetchTime; + if (timeSinceLastFetch >= minApiFetchInterval) { + fetchAvailableSchemesFromAPI(); + } else { + Logger.d("ColorSchemeDownload", "Cache empty but recent API call detected, skipping fetch"); + fetching = false; + } } + } catch (error) { + Logger.e("ColorSchemeDownload", "Failed to load schemes from cache:", error); + fetching = false; } } + function migrateFromOldSchemesList() { + const oldSchemesPath = Settings.cacheDir + "color-schemes-list.json"; + const migrationFileView = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + FileView { + id: migrationView + path: "${oldSchemesPath}" + printErrors: false + adapter: JsonAdapter { + property var schemes: [] + property real timestamp: 0 + } + onLoaded: { + root.availableSchemes = adapter.schemes || []; + root.saveSchemesToCache(); + Logger.i("ColorSchemeDownload", "Migrated color-schemes-list.json to ShellState"); + migrationView.destroy(); + } + onLoadFailed: { + migrationView.destroy(); + } + } + `, root, "schemesMigrationView"); + } + function saveSchemesToCache() { - schemesCacheAdapter.schemes = availableSchemes; - schemesCacheAdapter.timestamp = Time.timestamp; - - // Ensure cache directory exists - Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); - - Qt.callLater(() => { - schemesCacheFileView.writeAdapter(); - Logger.d("ColorSchemeDownload", "Schemes list saved to cache"); - }); + try { + ShellState.setColorSchemesList({ + schemes: availableSchemes, + timestamp: Time.timestamp + }); + Logger.d("ColorSchemeDownload", "Schemes list saved to ShellState"); + } catch (error) { + Logger.e("ColorSchemeDownload", "Failed to save schemes to cache:", error); + } } function fetchAvailableSchemes() { @@ -194,19 +204,11 @@ Popup { return; } - // Path is set when popup becomes visible, so FileView will start loading - // Try to load from cache first - if (schemesCacheFileView.loaded) { + // Try to load from ShellState cache first + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { loadSchemesFromCache(); - } else if (schemesCacheFileView.path) { - // Cache file path is set but not loaded yet, wait for it to load - // The FileView will trigger loadSchemesFromCache() when loaded - // But if it fails, we should fetch from API - if (!schemesCacheFileView.loading) { - schemesCacheFileView.reload(); - } } else { - // No cache file path, fetch directly from API + // ShellState not ready, fetch directly from API fetchAvailableSchemesFromAPI(); } } @@ -725,7 +727,25 @@ Popup { } onAvailableSchemesChanged: preFetchSchemeColors() - onVisibleChanged: preFetchSchemeColors() + onVisibleChanged: { + preFetchSchemeColors(); + + // Load schemes from ShellState when popup becomes visible + if (visible) { + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + loadSchemesFromCache(); + } + } + } + + Connections { + target: typeof ShellState !== 'undefined' ? ShellState : null + function onIsLoadedChanged() { + if (root.visible && ShellState.isLoaded) { + loadSchemesFromCache(); + } + } + } contentItem: ColumnLayout { id: contentColumn diff --git a/Modules/Panels/Settings/Tabs/LockScreenTab.qml b/Modules/Panels/Settings/Tabs/LockScreenTab.qml index 1ab01d31..46b9497d 100644 --- a/Modules/Panels/Settings/Tabs/LockScreenTab.qml +++ b/Modules/Panels/Settings/Tabs/LockScreenTab.qml @@ -21,13 +21,6 @@ ColumnLayout { checked: Settings.data.general.compactLockScreen onToggled: checked => Settings.data.general.compactLockScreen = checked } - - NToggle { - label: I18n.tr("settings.lock-screen.show-hibernate.label") - description: I18n.tr("settings.lock-screen.show-hibernate.description") - checked: Settings.data.general.showHibernateOnLockScreen - onToggled: checked => Settings.data.general.showHibernateOnLockScreen = checked - } NDivider { Layout.fillWidth: true diff --git a/Services/Compositor/CompositorService.qml b/Services/Compositor/CompositorService.qml index 3a4a8957..346a4f71 100644 --- a/Services/Compositor/CompositorService.qml +++ b/Services/Compositor/CompositorService.qml @@ -32,21 +32,26 @@ Singleton { // Backend service loader property var backend: null - // Cache file path - property string displayCachePath: "" - Component.onCompleted: { - // Setup cache path (needs Settings to be available) + // Load display scales from ShellState Qt.callLater(() => { - if (typeof Settings !== 'undefined' && Settings.cacheDir) { - displayCachePath = Settings.cacheDir + "display.json"; - displayCacheFileView.path = displayCachePath; + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + loadDisplayScalesFromState(); } }); detectCompositor(); } + Connections { + target: typeof ShellState !== 'undefined' ? ShellState : null + function onIsLoadedChanged() { + if (ShellState.isLoaded) { + loadDisplayScalesFromState(); + } + } + } + function detectCompositor() { const hyprlandSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE"); const niriSocket = Quickshell.env("NIRI_SOCKET"); @@ -99,29 +104,50 @@ Singleton { } } - // Cache FileView for display scales - FileView { - id: displayCacheFileView - printErrors: false - watchChanges: false - - adapter: JsonAdapter { - id: displayCacheAdapter - property var displays: ({}) - } - - onLoaded: { - // Load cached display scales - displayScales = displayCacheAdapter.displays || {}; + // Load display scales from ShellState + function loadDisplayScalesFromState() { + try { + const cached = ShellState.getDisplay(); + if (cached && Object.keys(cached).length > 0) { + displayScales = cached; + displayScalesLoaded = true; + Logger.d("CompositorService", "Loaded display scales from ShellState"); + } else { + // Try to migrate from old display.json if it exists + migrateFromOldDisplayFile(); + } + } catch (error) { + Logger.e("CompositorService", "Failed to load display scales:", error); displayScalesLoaded = true; - // Logger.i("CompositorService", "Loaded display scales from cache:", JSON.stringify(displayScales)) } + } - onLoadFailed: { - // Cache doesn't exist yet, will be created on first update - displayScalesLoaded = true; - // Logger.i("CompositorService", "No display cache found, will create on first update") - } + // Migration from old display.json file + function migrateFromOldDisplayFile() { + const oldDisplayPath = Settings.cacheDir + "display.json"; + const migrationFileView = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + FileView { + id: migrationView + path: "${oldDisplayPath}" + printErrors: false + adapter: JsonAdapter { + property var displays: ({}) + } + onLoaded: { + parent.displayScales = adapter.displays || {}; + parent.displayScalesLoaded = true; + parent.saveDisplayScalesToCache(); + Logger.i("CompositorService", "Migrated display.json to ShellState"); + migrationView.destroy(); + } + onLoadFailed: { + parent.displayScalesLoaded = true; + migrationView.destroy(); + } + } + `, root, "migrationFileView"); } // Hyprland backend component @@ -234,12 +260,12 @@ Singleton { // Save display scales to cache function saveDisplayScalesToCache() { - if (!displayCachePath) { - return; + try { + ShellState.setDisplay(displayScales); + Logger.d("CompositorService", "Saved display scales to ShellState"); + } catch (error) { + Logger.e("CompositorService", "Failed to save display scales:", error); } - - displayCacheAdapter.displays = displayScales; - displayCacheFileView.writeAdapter(); } // Public function to get scale for a specific display diff --git a/Services/Noctalia/UpdateService.qml b/Services/Noctalia/UpdateService.qml index 1b07e8a4..79fcf0e1 100644 --- a/Services/Noctalia/UpdateService.qml +++ b/Services/Noctalia/UpdateService.qml @@ -15,7 +15,6 @@ Singleton { readonly property bool isDevelopment: true readonly property string developmentSuffix: "-git" readonly property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + developmentSuffix}` - readonly property string changelogStateFile: Quickshell.env("NOCTALIA_CHANGELOG_STATE_FILE") || (Settings.cacheDir + "changelog-state.json") // URLs readonly property string discordUrl: "https://discord.noctalia.dev" @@ -50,30 +49,21 @@ Singleton { initialized = true; Logger.i("UpdateService", "Version:", root.currentVersion); + + // Load changelog state from ShellState + Qt.callLater(() => { + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + loadChangelogState(); + } + }); } - FileView { - id: changelogStateFileView - path: root.changelogStateFile - printErrors: false - onLoaded: loadChangelogState() - onLoadFailed: error => { - if (error === 2) { - // File doesn't exist, create it - debouncedSaveChangelogState(); - } else { - Logger.e("UpdateService", "Failed to load changelog state file:", error); + Connections { + target: typeof ShellState !== 'undefined' ? ShellState : null + function onIsLoadedChanged() { + if (ShellState.isLoaded) { + loadChangelogState(); } - changelogStateLoaded = true; - if (pendingShowRequest) { - pendingShowRequest = false; - Qt.callLater(root.showLatestChangelog); - } - } - - JsonAdapter { - id: changelogStateAdapter - property string lastSeenVersion: "" } } @@ -326,12 +316,21 @@ Singleton { function loadChangelogState() { try { - changelogLastSeenVersion = changelogStateAdapter.lastSeenVersion || ""; - if (!changelogLastSeenVersion && Settings.data && Settings.data.changelog && Settings.data.changelog.lastSeenVersion) { - changelogLastSeenVersion = Settings.data.changelog.lastSeenVersion; - debouncedSaveChangelogState(); - Logger.i("UpdateService", "Migrated changelog lastSeenVersion from settings to cache"); + const changelog = ShellState.getChangelogState(); + changelogLastSeenVersion = changelog.lastSeenVersion || ""; + + if (!changelogLastSeenVersion) { + // Try to migrate from old changelog-state.json + migrateFromOldChangelogFile(); + // Also try settings migration + if (!changelogLastSeenVersion && Settings.data && Settings.data.changelog && Settings.data.changelog.lastSeenVersion) { + changelogLastSeenVersion = Settings.data.changelog.lastSeenVersion; + debouncedSaveChangelogState(); + Logger.i("UpdateService", "Migrated changelog lastSeenVersion from settings to ShellState"); + } } + + Logger.d("UpdateService", "Loaded changelog state from ShellState"); } catch (error) { Logger.e("UpdateService", "Failed to load changelog state:", error); } @@ -342,6 +341,31 @@ Singleton { } } + function migrateFromOldChangelogFile() { + const oldChangelogPath = Settings.cacheDir + "changelog-state.json"; + const migrationFileView = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + FileView { + id: migrationView + path: "${oldChangelogPath}" + printErrors: false + adapter: JsonAdapter { + property string lastSeenVersion: "" + } + onLoaded: { + parent.changelogLastSeenVersion = adapter.lastSeenVersion || ""; + parent.debouncedSaveChangelogState(); + Logger.i("UpdateService", "Migrated changelog-state.json to ShellState"); + migrationView.destroy(); + } + onLoadFailed: { + migrationView.destroy(); + } + } + `, root, "changelogMigrationView"); + } + function debouncedSaveChangelogState() { // Queue a save and restart the debounce timer pendingSave = true; @@ -363,26 +387,16 @@ Singleton { saveInProgress = true; try { - changelogStateAdapter.lastSeenVersion = changelogLastSeenVersion || ""; + ShellState.setChangelogState({ + lastSeenVersion: changelogLastSeenVersion || "" + }); + Logger.d("UpdateService", "Saved changelog state to ShellState"); + saveInProgress = false; - // Ensure cache directory exists - Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); - - // Small delay to ensure directory creation completes - Qt.callLater(() => { - try { - changelogStateFileView.writeAdapter(); - saveInProgress = false; - - // Check if another save was queued while we were saving - if (pendingSave) { - Qt.callLater(executeSave); - } - } catch (writeError) { - Logger.e("UpdateService", "Failed to write changelog state:", writeError); - saveInProgress = false; - } - }); + // Check if another save was queued while we were saving + if (pendingSave) { + Qt.callLater(executeSave); + } } catch (error) { Logger.e("UpdateService", "Failed to save changelog state:", error); saveInProgress = false; diff --git a/Services/System/NotificationService.qml b/Services/System/NotificationService.qml index 59ff6ad3..4f7b7a5a 100644 --- a/Services/System/NotificationService.qml +++ b/Services/System/NotificationService.qml @@ -17,7 +17,6 @@ Singleton { property int maxVisible: 5 property int maxHistory: 100 property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json") - property string stateFile: Settings.cacheDir + "notifications-state.json" // State property real lastSeenTs: 0 @@ -138,6 +137,22 @@ Singleton { if (Settings.isLoaded) { updateNotificationServer(); } + + // Load state from ShellState + Qt.callLater(() => { + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + loadState(); + } + }); + } + + Connections { + target: typeof ShellState !== 'undefined' ? ShellState : null + function onIsLoadedChanged() { + if (ShellState.isLoaded) { + loadState(); + } + } } Connections { @@ -471,22 +486,6 @@ Singleton { } } - // Persistence - State - FileView { - id: stateFileView - path: stateFile - printErrors: false - onLoaded: loadState() - onLoadFailed: error => { - if (error === 2) - writeAdapter(); - } - - JsonAdapter { - id: stateAdapter - property real lastSeenTs: 0 - } - } Timer { id: saveTimer @@ -546,22 +545,57 @@ Singleton { function loadState() { try { - root.lastSeenTs = stateAdapter.lastSeenTs || 0; + const notifState = ShellState.getNotificationsState(); + root.lastSeenTs = notifState.lastSeenTs || 0; - if (root.lastSeenTs === 0 && Settings.data.notifications && Settings.data.notifications.lastSeenTs) { - root.lastSeenTs = Settings.data.notifications.lastSeenTs; - saveState(); - Logger.i("Notifications", "Migrated lastSeenTs from settings to state file"); + if (root.lastSeenTs === 0) { + // Try to migrate from old notifications-state.json + migrateFromOldStateFile(); + // Also try settings migration + if (root.lastSeenTs === 0 && Settings.data.notifications && Settings.data.notifications.lastSeenTs) { + root.lastSeenTs = Settings.data.notifications.lastSeenTs; + saveState(); + Logger.i("Notifications", "Migrated lastSeenTs from settings to ShellState"); + } } + + Logger.d("Notifications", "Loaded state from ShellState"); } catch (e) { Logger.e("Notifications", "Load state failed:", e); } } + function migrateFromOldStateFile() { + const oldStatePath = Settings.cacheDir + "notifications-state.json"; + const migrationFileView = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + FileView { + id: migrationView + path: "${oldStatePath}" + printErrors: false + adapter: JsonAdapter { + property real lastSeenTs: 0 + } + onLoaded: { + parent.lastSeenTs = adapter.lastSeenTs || 0; + parent.saveState(); + Logger.i("Notifications", "Migrated notifications-state.json to ShellState"); + migrationView.destroy(); + } + onLoadFailed: { + migrationView.destroy(); + } + } + `, root, "notificationMigrationView"); + } + function saveState() { try { - stateAdapter.lastSeenTs = root.lastSeenTs; - stateFileView.writeAdapter(); + ShellState.setNotificationsState({ + lastSeenTs: root.lastSeenTs + }); + Logger.d("Notifications", "Saved state to ShellState"); } catch (e) { Logger.e("Notifications", "Save state failed:", e); } diff --git a/Services/System/ProgramCheckerService.qml b/Services/System/ProgramCheckerService.qml index a0c9d644..7974a753 100644 --- a/Services/System/ProgramCheckerService.qml +++ b/Services/System/ProgramCheckerService.qml @@ -27,7 +27,6 @@ Singleton { property bool codeAvailable: false property bool gnomeCalendarAvailable: false property bool spicetifyAvailable: false - property bool telegramAvailable: false // Discord client auto-detection property var availableDiscordClients: [] @@ -182,8 +181,7 @@ Singleton { "wlsunsetAvailable": ["which", "wlsunset"], "codeAvailable": ["which", "code"], "gnomeCalendarAvailable": ["which", "gnome-calendar"], - "spicetifyAvailable": ["which", "spicetify"], - "telegramAvailable": ["which", "telegram-desktop"] + "spicetifyAvailable": ["which", "spicetify"] }) // Internal tracking diff --git a/Services/Theming/TemplateRegistry.qml b/Services/Theming/TemplateRegistry.qml index c9721cb3..4322ba44 100644 --- a/Services/Theming/TemplateRegistry.qml +++ b/Services/Theming/TemplateRegistry.qml @@ -208,17 +208,6 @@ Singleton { } ], "postProcess": () => `spicetify -q apply --no-restart` - }, - { - "id": "telegram", - "name": "Telegram", - "category": "applications", - "input": "telegram.tdesktop-theme", - "outputs": [ - { - "path": "~/.config/telegram-desktop/themes/noctalia.tdesktop-theme" - } - ] } ] diff --git a/Services/UI/WallpaperService.qml b/Services/UI/WallpaperService.qml index b138e398..8ed29d38 100644 --- a/Services/UI/WallpaperService.qml +++ b/Services/UI/WallpaperService.qml @@ -83,18 +83,42 @@ Singleton { translateModels(); - // Rebuild cache from settings + // Load wallpapers from ShellState first (faster), then fall back to Settings currentWallpapers = ({}); + + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + var cachedWallpapers = ShellState.getWallpapers(); + if (cachedWallpapers && Object.keys(cachedWallpapers).length > 0) { + currentWallpapers = cachedWallpapers; + Logger.d("Wallpaper", "Loaded wallpapers from ShellState"); + } else { + // Fall back to Settings if ShellState is empty + loadFromSettings(); + } + } else { + // ShellState not ready yet, load from Settings + loadFromSettings(); + } + + isInitialized = true; + Logger.d("Wallpaper", "Triggering initial wallpaper scan"); + Qt.callLater(refreshWallpapersList); + } + + function loadFromSettings() { var monitors = Settings.data.wallpaper.monitors || []; for (var i = 0; i < monitors.length; i++) { if (monitors[i].name && monitors[i].wallpaper) { currentWallpapers[monitors[i].name] = monitors[i].wallpaper; } } - - isInitialized = true; - Logger.d("Wallpaper", "Triggering initial wallpaper scan"); - Qt.callLater(refreshWallpapersList); + Logger.d("Wallpaper", "Loaded wallpapers from Settings"); + + // Migrate to ShellState if we loaded from Settings + if (typeof ShellState !== 'undefined' && ShellState.isLoaded && Object.keys(currentWallpapers).length > 0) { + ShellState.setWallpapers(currentWallpapers); + Logger.i("Wallpaper", "Migrated wallpaper paths from Settings to ShellState"); + } } // ------------------------------------------------- @@ -270,33 +294,11 @@ Singleton { // Update cache directly currentWallpapers[screenName] = path; - // Update Settings - still need immutable update for Settings persistence - // The slice() ensures Settings detects the change and saves properly - var monitors = Settings.data.wallpaper.monitors || []; - var found = false; - - var newMonitors = monitors.map(function (monitor) { - if (monitor.name === screenName) { - found = true; - return { - "name": screenName, - "directory": Settings.preprocessPath(monitor.directory) || getMonitorDirectory(screenName), - "wallpaper": path - }; - } - return monitor; - }); - - if (!found) { - newMonitors.push({ - "name": screenName, - "directory": getMonitorDirectory(screenName), - "wallpaper": path - }); + // Save to ShellState (wallpaper paths now only stored here, not in Settings) + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + ShellState.setWallpapers(currentWallpapers); } - Settings.data.wallpaper.monitors = newMonitors.slice(); - // Emit signal for this specific wallpaper change root.wallpaperChanged(screenName, path); diff --git a/shell.qml b/shell.qml index 74b4710d..19f35a29 100644 --- a/shell.qml +++ b/shell.qml @@ -37,6 +37,7 @@ ShellRoot { property bool i18nLoaded: false property bool settingsLoaded: false + property bool shellStateLoaded: false Component.onCompleted: { Logger.i("Shell", "---------------------------"); @@ -63,8 +64,18 @@ ShellRoot { settingsLoaded = true; } } + + Connections { + target: typeof ShellState !== 'undefined' ? ShellState : null + function onIsLoadedChanged() { + if (ShellState.isLoaded) { + shellStateLoaded = true; + } + } + } + Loader { - active: i18nLoaded && settingsLoaded + active: i18nLoaded && settingsLoaded && shellStateLoaded sourceComponent: Item { Component.onCompleted: {