mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2025-12-06 06:36:15 +00:00
initial commit
This commit is contained in:
@@ -70,7 +70,6 @@
|
||||
"animationDisabled": false,
|
||||
"compactLockScreen": false,
|
||||
"lockOnSuspend": true,
|
||||
"showHibernateOnLockScreen": true,
|
||||
"enableShadows": true,
|
||||
"shadowDirection": "bottom_right",
|
||||
"shadowOffsetX": 2,
|
||||
|
||||
@@ -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
183
Commons/ShellState.qml
Normal 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 || {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
13
shell.qml
13
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: {
|
||||
|
||||
Reference in New Issue
Block a user