initial commit

This commit is contained in:
Ly-sec
2025-11-22 13:51:58 +01:00
parent 01a26fd910
commit 74ba883dd8
14 changed files with 515 additions and 265 deletions

View File

@@ -70,7 +70,6 @@
"animationDisabled": false,
"compactLockScreen": false,
"lockOnSuspend": true,
"showHibernateOnLockScreen": true,
"enableShadows": true,
"shadowDirection": "bottom_right",
"shadowOffsetX": 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
}

183
Commons/ShellState.qml Normal file
View File

@@ -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 || {};
}
}

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
@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

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,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

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,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;

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,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);
}

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

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"
}
]
}
]

View File

@@ -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);

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: {