Merge branch 'main' of github.com:noctalia-dev/noctalia-shell

This commit is contained in:
ItsLemmy
2025-11-22 11:23:48 -05:00
14 changed files with 601 additions and 244 deletions
-1
View File
@@ -70,7 +70,6 @@
"animationDisabled": false,
"compactLockScreen": false,
"lockOnSuspend": true,
"showHibernateOnLockScreen": true,
"enableShadows": true,
"shadowDirection": "bottom_right",
"shadowOffsetX": 2,
+214 -2
View File
@@ -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
}
@@ -796,6 +794,220 @@ Singleton {
Logger.w("Settings", "Failed to read raw JSON for dimDesktop migration:", error);
}
}
// -----------------
// 8th. Migrate ShellState-related files from old cache files to ShellState
// This consolidates migrations that were previously in individual service files
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
migrateShellStateFiles();
} else {
// Wait for ShellState to be ready
Qt.callLater(() => {
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
migrateShellStateFiles();
}
});
}
}
// -----------------------------------------------------
// Migrate old cache files to ShellState
function migrateShellStateFiles() {
// Migrate display.json → ShellState (CompositorService)
migrateDisplayFile();
// Migrate notifications-state.json → ShellState (NotificationService)
migrateNotificationsStateFile();
// Migrate changelog-state.json → ShellState (UpdateService)
migrateChangelogStateFile();
// Migrate color-schemes-list.json → ShellState (SchemeDownloader)
migrateColorSchemesListFile();
// Migrate wallpaper paths from Settings → ShellState (WallpaperService)
migrateWallpaperPaths();
}
function migrateDisplayFile() {
// Check if ShellState already has display data
const cached = ShellState.getDisplay();
if (cached && Object.keys(cached).length > 0) {
return; // Already migrated
}
const oldDisplayPath = cacheDir + "display.json";
const migrationFileView = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
import qs.Commons
FileView {
id: migrationView
path: "${oldDisplayPath}"
printErrors: false
adapter: JsonAdapter {
property var displays: ({})
}
onLoaded: {
if (adapter.displays && Object.keys(adapter.displays).length > 0) {
ShellState.setDisplay(adapter.displays);
Logger.i("Settings", "Migrated display.json to ShellState");
}
migrationView.destroy();
}
onLoadFailed: {
migrationView.destroy();
}
}
`, root, "displayMigrationView");
}
function migrateNotificationsStateFile() {
// Check if ShellState already has notifications state
const cached = ShellState.getNotificationsState();
if (cached && cached.lastSeenTs && cached.lastSeenTs > 0) {
return; // Already migrated
}
// Also check Settings for lastSeenTs
if (adapter.notifications && adapter.notifications.lastSeenTs) {
ShellState.setNotificationsState({
lastSeenTs: adapter.notifications.lastSeenTs
});
Logger.i("Settings", "Migrated notifications lastSeenTs from Settings to ShellState");
return;
}
const oldStatePath = cacheDir + "notifications-state.json";
const migrationFileView = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
import qs.Commons
FileView {
id: migrationView
path: "${oldStatePath}"
printErrors: false
adapter: JsonAdapter {
property real lastSeenTs: 0
}
onLoaded: {
if (adapter.lastSeenTs && adapter.lastSeenTs > 0) {
ShellState.setNotificationsState({
lastSeenTs: adapter.lastSeenTs
});
Logger.i("Settings", "Migrated notifications-state.json to ShellState");
}
migrationView.destroy();
}
onLoadFailed: {
migrationView.destroy();
}
}
`, root, "notificationsMigrationView");
}
function migrateChangelogStateFile() {
// Check if ShellState already has changelog state
const cached = ShellState.getChangelogState();
if (cached && cached.lastSeenVersion && cached.lastSeenVersion !== "") {
return; // Already migrated
}
// Also check Settings for lastSeenVersion
if (adapter.changelog && adapter.changelog.lastSeenVersion) {
ShellState.setChangelogState({
lastSeenVersion: adapter.changelog.lastSeenVersion
});
Logger.i("Settings", "Migrated changelog lastSeenVersion from Settings to ShellState");
return;
}
const oldChangelogPath = cacheDir + "changelog-state.json";
const migrationFileView = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
import qs.Commons
FileView {
id: migrationView
path: "${oldChangelogPath}"
printErrors: false
adapter: JsonAdapter {
property string lastSeenVersion: ""
}
onLoaded: {
if (adapter.lastSeenVersion && adapter.lastSeenVersion !== "") {
ShellState.setChangelogState({
lastSeenVersion: adapter.lastSeenVersion
});
Logger.i("Settings", "Migrated changelog-state.json to ShellState");
}
migrationView.destroy();
}
onLoadFailed: {
migrationView.destroy();
}
}
`, root, "changelogMigrationView");
}
function migrateColorSchemesListFile() {
// Check if ShellState already has color schemes list
const cached = ShellState.getColorSchemesList();
if (cached && cached.schemes && cached.schemes.length > 0) {
return; // Already migrated
}
const oldSchemesPath = cacheDir + "color-schemes-list.json";
const migrationFileView = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
import qs.Commons
FileView {
id: migrationView
path: "${oldSchemesPath}"
printErrors: false
adapter: JsonAdapter {
property var schemes: []
property real timestamp: 0
}
onLoaded: {
if (adapter.schemes && adapter.schemes.length > 0) {
ShellState.setColorSchemesList({
schemes: adapter.schemes,
timestamp: adapter.timestamp || 0
});
Logger.i("Settings", "Migrated color-schemes-list.json to ShellState");
}
migrationView.destroy();
}
onLoadFailed: {
migrationView.destroy();
}
}
`, root, "schemesMigrationView");
}
function migrateWallpaperPaths() {
// Check if ShellState already has wallpaper paths
const cached = ShellState.getWallpapers();
if (cached && Object.keys(cached).length > 0) {
return; // Already migrated
}
// Migrate from Settings wallpaper.monitors
var monitors = adapter.wallpaper.monitors || [];
if (monitors.length > 0) {
var wallpapers = {};
for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name && monitors[i].wallpaper) {
wallpapers[monitors[i].name] = monitors[i].wallpaper;
}
}
if (Object.keys(wallpapers).length > 0) {
ShellState.setWallpapers(wallpapers);
Logger.i("Settings", "Migrated wallpaper paths from Settings to ShellState");
}
}
}
// -----------------------------------------------------
+182
View File
@@ -0,0 +1,182 @@
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 || {};
}
}
+10 -9
View File
@@ -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
@@ -525,7 +525,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
}
@@ -551,7 +551,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 {
@@ -750,7 +750,7 @@ Loader {
}
}
// Forecast
// 3-day forecast
RowLayout {
visible: Settings.data.location.weatherEnabled && LocationService.data.weather !== null
Layout.preferredWidth: 260
@@ -758,7 +758,7 @@ Loader {
spacing: 4
Repeater {
model: MediaService.currentPlayer && MediaService.canPlay ? 3 : 4
model: 3
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: 3
@@ -805,6 +805,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)
@@ -1182,7 +1184,6 @@ Loader {
}
Rectangle {
visible: Settings.data.general.showHibernateOnLockScreen
Layout.fillWidth: true
Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
@@ -853,6 +853,7 @@ ColumnLayout {
}
}
}
}
// Miscellaneous
NCollapsible {
@@ -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,34 +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
radius: Style.radiusL
@@ -135,58 +106,67 @@ 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)) {
// Migration is now handled in Settings.qml
// 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 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 +174,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 +697,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
+31 -33
View File
@@ -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,28 +104,21 @@ 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 {
// Migration is now handled in Settings.qml
displayScalesLoaded = true;
}
} 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")
}
}
@@ -234,12 +232,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
+26 -47
View File
@@ -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,11 @@ 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 || "";
// Migration is now handled in Settings.qml
Logger.d("UpdateService", "Loaded changelog state from ShellState");
} catch (error) {
Logger.e("UpdateService", "Failed to load changelog state:", error);
}
@@ -363,26 +352,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;
+24 -26
View File
@@ -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,23 +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
interval: 200
@@ -546,13 +544,11 @@ 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");
}
// Migration is now handled in Settings.qml
Logger.d("Notifications", "Loaded state from ShellState");
} catch (e) {
Logger.e("Notifications", "Load state failed:", e);
}
@@ -560,8 +556,10 @@ Singleton {
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);
}
+1 -3
View File
@@ -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
+1
View File
@@ -213,6 +213,7 @@ Singleton {
FileView {
id: colorsWriter
path: colorsJsonFilePath
printErrors: false
onSaved:
// Logger.i("ColorScheme", "Colors saved")
-11
View File
@@ -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"
}
]
}
]
+27 -29
View File
@@ -83,18 +83,38 @@ 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;
}
}
Logger.d("Wallpaper", "Loaded wallpapers from Settings");
isInitialized = true;
Logger.d("Wallpaper", "Triggering initial wallpaper scan");
Qt.callLater(refreshWallpapersList);
// Migration is now handled in Settings.qml
}
// -------------------------------------------------
@@ -270,33 +290,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);
+12 -1
View File
@@ -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: {