qs: better background blur & update properties to readonly in various services and components

This commit is contained in:
2026-03-07 05:42:03 +01:00
parent fda874e628
commit ad6486c141
9 changed files with 161 additions and 343 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"editor.formatOnSave": false "editor.formatOnSave": true
} }
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Constants import qs.Constants
@@ -8,9 +9,20 @@ Variants {
model: Quickshell.screens model: Quickshell.screens
Item { Item {
id: root
property var modelData property var modelData
readonly property color tintColor: BackgroundService.tintColor
readonly property real tintOpacity: BackgroundService.tintOpacity
readonly property real blurPercentage: BackgroundService.blurPercentage
readonly property real blurRadius: BackgroundService.blurRadius
PanelWindow { PanelWindow {
id: bgWindow
readonly property bool doBlur: BarService.focusMode && (BackgroundService.previewPath === "")
readonly property string imagePath: BackgroundService.previewPath || BackgroundService.cachedPath
screen: modelData screen: modelData
WlrLayershell.namespace: "quickshell-background" WlrLayershell.namespace: "quickshell-background"
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Background
@@ -27,29 +39,33 @@ Variants {
anchors.fill: parent anchors.fill: parent
color: Colors.mSurface color: Colors.mSurface
Item {
anchors.fill: parent
Item { Item {
id: bgManager id: bgManager
property string activeSource: BackgroundService.previewPath || (BarService.focusMode ? BackgroundService.cachedBlurredPath : BackgroundService.cachedPath) property string activeSource: bgWindow.imagePath
property bool showFirst: true property bool showFirst: true
anchors.fill: parent anchors.fill: parent
visible: false
onActiveSourceChanged: { onActiveSourceChanged: {
showFirst = !showFirst; showFirst = !showFirst;
if (showFirst) if (showFirst)
img1.source = activeSource; bgImg1.source = activeSource;
else else
img2.source = activeSource; bgImg2.source = activeSource;
} }
Component.onCompleted: { Component.onCompleted: {
if (showFirst) if (showFirst)
img1.source = activeSource; bgImg1.source = activeSource;
else else
img2.source = activeSource; bgImg2.source = activeSource;
} }
Image { Image {
id: img1 id: bgImg1
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
@@ -65,7 +81,7 @@ Variants {
} }
Image { Image {
id: img2 id: bgImg2
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
@@ -82,11 +98,43 @@ Variants {
} }
MultiEffect {
source: bgManager
anchors.fill: bgManager
colorizationColor: root.tintColor
colorization: bgWindow.doBlur ? root.tintOpacity : 0
blurEnabled: true
blur: bgWindow.doBlur ? root.blurPercentage : 0
blurMax: root.blurRadius
Behavior on blur {
NumberAnimation {
duration: Style.animationSlow
}
}
Behavior on colorization {
NumberAnimation {
duration: Style.animationSlow
}
}
}
}
} }
} }
PanelWindow { PanelWindow {
id: bdWindow
property bool doBlur: true
property string imagePath: BackgroundService.cachedPath
screen: modelData screen: modelData
WlrLayershell.namespace: "quickshell-backdrop" WlrLayershell.namespace: "quickshell-backdrop"
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Background
@@ -103,29 +151,33 @@ Variants {
anchors.fill: parent anchors.fill: parent
color: Colors.mSurface color: Colors.mSurface
Item {
anchors.fill: parent
Item { Item {
id: backdropManager id: backdropManager
property string activeSource: BackgroundService.cachedBlurredPath property string activeSource: bdWindow.imagePath
property bool showFirst: true property bool showFirst: true
anchors.fill: parent anchors.fill: parent
visible: false
onActiveSourceChanged: { onActiveSourceChanged: {
showFirst = !showFirst; showFirst = !showFirst;
if (showFirst) if (showFirst)
backImg1.source = activeSource; bdImg1.source = activeSource;
else else
backImg2.source = activeSource; bdImg2.source = activeSource;
} }
Component.onCompleted: { Component.onCompleted: {
if (showFirst) if (showFirst)
backImg1.source = activeSource; bdImg1.source = activeSource;
else else
backImg2.source = activeSource; bdImg2.source = activeSource;
} }
Image { Image {
id: backImg1 id: bdImg1
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
@@ -141,7 +193,7 @@ Variants {
} }
Image { Image {
id: backImg2 id: bdImg2
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
@@ -158,6 +210,33 @@ Variants {
} }
MultiEffect {
source: backdropManager
anchors.fill: backdropManager
colorizationColor: root.tintColor
colorization: bdWindow.doBlur ? root.tintOpacity : 0
blurEnabled: true
blur: bdWindow.doBlur ? root.blurPercentage : 0
blurMax: root.blurRadius
Behavior on blur {
NumberAnimation {
duration: Style.animationSlow
}
}
Behavior on colorization {
NumberAnimation {
duration: Style.animationSlow
}
}
}
}
} }
} }
@@ -8,7 +8,7 @@ import qs.Services
UBox { UBox {
id: root id: root
property string currentPanel: ShellState.leftSiderbarTab // "bluetooth", "wifi" readonly property string currentPanel: ShellState.leftSiderbarTab // "bluetooth", "wifi"
implicitHeight: (root.currentPanel === "bluetooth" ? btContentLoader.implicitHeight : wifiContentLoader.implicitHeight) + toggleGroup.implicitHeight + Style.marginXS * 2 + Style.marginS * 2 implicitHeight: (root.currentPanel === "bluetooth" ? btContentLoader.implicitHeight : wifiContentLoader.implicitHeight) + toggleGroup.implicitHeight + Style.marginXS * 2 + Style.marginS * 2
@@ -8,7 +8,7 @@ import qs.Services
Item { Item {
id: root id: root
property string currentPanel: ShellState.rightSiderbarTab // "notifications", "notes" readonly property string currentPanel: ShellState.rightSiderbarTab // "notifications", "notes"
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@@ -8,14 +8,15 @@ pragma Singleton
Singleton { Singleton {
id: root id: root
property string backgroundWidth: "2560" readonly property string backgroundWidth: "2560"
property string backgroundHeight: "1440" readonly property string backgroundHeight: "1440"
property string cachedPath: "" property string cachedPath: ""
property string cachedBlurredPath: ""
property string previewPath: "" property string previewPath: ""
// Preserved for getBlurredOverview // Preserved for getBlurredOverview
property string tintColor: Colors.mSurface readonly property string tintColor: Colors.mSurface
property bool isDarkMode: false readonly property real tintOpacity: 0.5
readonly property real blurPercentage: 1
readonly property real blurRadius: 32
function loadBackground() { function loadBackground() {
if (!SettingsService.backgroundPath) { if (!SettingsService.backgroundPath) {
@@ -29,14 +30,6 @@ Singleton {
} }
cachedPath = path; cachedPath = path;
Logger.i("BackgroundService", "Loaded background image as cached path: " + path); Logger.i("BackgroundService", "Loaded background image as cached path: " + path);
ImageCacheService.getBlurredOverview(SettingsService.backgroundPath, backgroundWidth, backgroundHeight, tintColor, isDarkMode, function(blurredPath) {
if (!blurredPath) {
Logger.e("BackgroundService", "Failed to load blurred background image from path: " + SettingsService.backgroundPath);
return ;
}
cachedBlurredPath = blurredPath;
Logger.i("BackgroundService", "Loaded blurred background image as cached blurred path: " + blurredPath);
});
}); });
} }
@@ -63,17 +56,19 @@ Singleton {
if (!exists) if (!exists)
return ; return ;
SettingsService.backgroundPath = path; loadWallpaperDebouncer.pendingPath = path;
loadWallpaperDebouncer.start(); loadWallpaperDebouncer.start();
}); });
} }
Component.onCompleted: { Component.onCompleted: {
loadWallpaperDebouncer.pendingPath = SettingsService.backgroundPath;
loadWallpaperDebouncer.start(); loadWallpaperDebouncer.start();
} }
Connections { Connections {
function onBackgroundPathChanged() { function onBackgroundPathChanged() {
loadWallpaperDebouncer.pendingPath = SettingsService.backgroundPath;
loadWallpaperDebouncer.start(); loadWallpaperDebouncer.start();
} }
@@ -83,10 +78,13 @@ Singleton {
Timer { Timer {
id: loadWallpaperDebouncer id: loadWallpaperDebouncer
property string pendingPath: ""
interval: 200 interval: 200
running: false running: false
repeat: false repeat: false
onTriggered: { onTriggered: {
SettingsService.backgroundPath = pendingPath;
root.loadBackground(); root.loadBackground();
} }
} }
@@ -17,11 +17,8 @@ Singleton {
property bool initialized: false property bool initialized: false
// Cache directories // Cache directories
readonly property string baseDir: Paths.cacheDir + "images/" readonly property string baseDir: Paths.cacheDir + "images/"
readonly property string wpThumbDir: baseDir + "wallpapers/thumbnails/"
readonly property string wpLargeDir: baseDir + "wallpapers/large/" readonly property string wpLargeDir: baseDir + "wallpapers/large/"
readonly property string wpOverviewDir: baseDir + "wallpapers/overview/"
readonly property string notificationsDir: baseDir + "notifications/" readonly property string notificationsDir: baseDir + "notifications/"
readonly property string contributorsDir: baseDir + "contributors/"
// Supported image formats - extended list when ImageMagick is available // Supported image formats - extended list when ImageMagick is available
readonly property var basicImageFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp"] readonly property var basicImageFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp"]
readonly property var extendedImageFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp", "*.webp", "*.avif", "*.heic", "*.heif", "*.tiff", "*.tif", "*.pnm", "*.pgm", "*.ppm", "*.pbm", "*.svg", "*.svgz", "*.ico", "*.icns", "*.jxl", "*.jp2", "*.j2k", "*.exr", "*.hdr", "*.dds", "*.tga"] readonly property var extendedImageFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp", "*.webp", "*.avif", "*.heic", "*.heif", "*.tiff", "*.tif", "*.pnm", "*.pgm", "*.ppm", "*.pbm", "*.svg", "*.svgz", "*.ico", "*.icns", "*.jxl", "*.jp2", "*.j2k", "*.exr", "*.hdr", "*.dds", "*.tga"]
@@ -67,41 +64,18 @@ Singleton {
} }
function createDirectories() { function createDirectories() {
Quickshell.execDetached(["mkdir", "-p", wpThumbDir]);
Quickshell.execDetached(["mkdir", "-p", wpLargeDir]); Quickshell.execDetached(["mkdir", "-p", wpLargeDir]);
Quickshell.execDetached(["mkdir", "-p", wpOverviewDir]);
Quickshell.execDetached(["mkdir", "-p", notificationsDir]); Quickshell.execDetached(["mkdir", "-p", notificationsDir]);
Quickshell.execDetached(["mkdir", "-p", contributorsDir]);
} }
function cleanupOldCache() { function cleanupOldCache() {
const dirs = [wpThumbDir, wpLargeDir, wpOverviewDir, notificationsDir, contributorsDir]; const dirs = [wpLargeDir, notificationsDir];
dirs.forEach(function(dir) { dirs.forEach(function(dir) {
Quickshell.execDetached(["find", dir, "-type", "f", "-mtime", "+30", "-delete"]); Quickshell.execDetached(["find", dir, "-type", "f", "-mtime", "+30", "-delete"]);
}); });
Logger.d("ImageCache", "Cleanup triggered for files older than 30 days"); Logger.d("ImageCache", "Cleanup triggered for files older than 30 days");
} }
// -------------------------------------------------
// Public API: Get Thumbnail (384x384)
// -------------------------------------------------
function getThumbnail(sourcePath, callback) {
if (!sourcePath || sourcePath === "") {
callback("", false);
return ;
}
getMtime(sourcePath, function(mtime) {
const cacheKey = generateThumbnailKey(sourcePath, mtime);
const cachedPath = wpThumbDir + cacheKey + ".png";
processRequest(cacheKey, cachedPath, sourcePath, callback, function() {
if (imageMagickAvailable)
startThumbnailProcessing(sourcePath, cachedPath, cacheKey);
else
queueFallbackProcessing(sourcePath, cachedPath, cacheKey, 384);
});
});
}
// ------------------------------------------------- // -------------------------------------------------
// Public API: Get Large Image (scaled to specified dimensions) // Public API: Get Large Image (scaled to specified dimensions)
// ------------------------------------------------- // -------------------------------------------------
@@ -209,57 +183,9 @@ Singleton {
}); });
} }
// -------------------------------------------------
// Public API: Get Circular Avatar (256x256)
// -------------------------------------------------
function getCircularAvatar(url, username, callback) {
if (!url || !username) {
callback("", false);
return ;
}
const cacheKey = username;
const cachedPath = contributorsDir + username + "_circular.png";
processRequest(cacheKey, cachedPath, url, callback, function() {
if (imageMagickAvailable) {
downloadAndProcessAvatar(url, username, cachedPath, cacheKey);
} else {
// No fallback for circular avatars without ImageMagick
Logger.w("ImageCache", "Circular avatars require ImageMagick");
notifyCallbacks(cacheKey, "", false);
}
});
}
// -------------------------------------------------
// Public API: Get Blurred Overview (for Niri overview background)
// -------------------------------------------------
function getBlurredOverview(sourcePath, width, height, tintColor, isDarkMode, callback) {
if (!sourcePath || sourcePath === "") {
callback("", false);
return ;
}
if (!imageMagickAvailable) {
Logger.d("ImageCache", "ImageMagick not available for overview blur, using original:", sourcePath);
callback(sourcePath, false);
return ;
}
getMtime(sourcePath, function(mtime) {
const cacheKey = generateOverviewKey(sourcePath, width, height, tintColor, isDarkMode, mtime);
const cachedPath = wpOverviewDir + cacheKey + ".png";
processRequest(cacheKey, cachedPath, sourcePath, callback, function() {
startOverviewProcessing(sourcePath, cachedPath, width, height, tintColor, isDarkMode, cacheKey);
});
});
}
// ------------------------------------------------- // -------------------------------------------------
// Cache Key Generation // Cache Key Generation
// ------------------------------------------------- // -------------------------------------------------
function generateThumbnailKey(sourcePath, mtime) {
const keyString = sourcePath + "@384x384@" + (mtime || "unknown");
return Checksum.sha256(keyString);
}
function generateLargeKey(sourcePath, width, height, mtime) { function generateLargeKey(sourcePath, width, height, mtime) {
const keyString = sourcePath + "@" + width + "x" + height + "@" + (mtime || "unknown"); const keyString = sourcePath + "@" + width + "x" + height + "@" + (mtime || "unknown");
return Checksum.sha256(keyString); return Checksum.sha256(keyString);
@@ -272,11 +198,6 @@ Singleton {
return Checksum.sha256(imageUri); return Checksum.sha256(imageUri);
} }
function generateOverviewKey(sourcePath, width, height, tintColor, isDarkMode, mtime) {
const keyString = sourcePath + "@" + width + "x" + height + "@" + tintColor + "@" + (isDarkMode ? "dark" : "light") + "@" + (mtime || "unknown");
return Checksum.sha256(keyString);
}
// ------------------------------------------------- // -------------------------------------------------
// Request Processing (with coalescing) // Request Processing (with coalescing)
// ------------------------------------------------- // -------------------------------------------------
@@ -325,17 +246,6 @@ Singleton {
processingFailed(cacheKey, "Processing failed"); processingFailed(cacheKey, "Processing failed");
} }
// -------------------------------------------------
// ImageMagick Processing: Thumbnail
// -------------------------------------------------
function startThumbnailProcessing(sourcePath, outputPath, cacheKey) {
const srcEsc = sourcePath.replace(/'/g, "'\\''");
const dstEsc = outputPath.replace(/'/g, "'\\''");
// Use Lanczos filter for high-quality downscaling, subtle unsharp mask, and PNG for lossless output
const command = `magick '${srcEsc}' -auto-orient -filter Lanczos -resize '384x384^' -gravity center -extent 384x384 -unsharp 0x0.5 '${dstEsc}'`;
runProcess(command, cacheKey, outputPath, sourcePath);
}
// ------------------------------------------------- // -------------------------------------------------
// ImageMagick Processing: Large // ImageMagick Processing: Large
// ------------------------------------------------- // -------------------------------------------------
@@ -347,79 +257,6 @@ Singleton {
runProcess(command, cacheKey, outputPath, sourcePath); runProcess(command, cacheKey, outputPath, sourcePath);
} }
// -------------------------------------------------
// ImageMagick Processing: Blurred Overview
// -------------------------------------------------
function startOverviewProcessing(sourcePath, outputPath, width, height, tintColor, isDarkMode, cacheKey) {
const srcEsc = sourcePath.replace(/'/g, "'\\''");
const dstEsc = outputPath.replace(/'/g, "'\\''");
// Resize, blur, then tint overlay
const command = `magick '${srcEsc}' -auto-orient -resize '${width}x${height}' -gravity center -blur 0x20 \\( +clone -fill '${tintColor}' -colorize 100 -alpha set -channel A -evaluate set 50% +channel \\) -composite '${dstEsc}'`;
runProcess(command, cacheKey, outputPath, sourcePath);
}
// -------------------------------------------------
// ImageMagick Processing: Circular Avatar
// -------------------------------------------------
function downloadAndProcessAvatar(url, username, outputPath, cacheKey) {
const tempPath = contributorsDir + username + "_temp.png";
const tempEsc = tempPath.replace(/'/g, "'\\''");
const urlEsc = url.replace(/'/g, "'\\''");
// Download first (uses utility queue since curl/wget are lightweight)
const downloadCmd = `curl -L -s -o '${tempEsc}' '${urlEsc}' || wget -q -O '${tempEsc}' '${urlEsc}'`;
const processString = `
import QtQuick
import Quickshell.Io
Process {
command: ["sh", "-c", "${downloadCmd.replace(/"/g, '\\"')}"]
stdout: StdioCollector {}
stderr: StdioCollector {}
}
`;
queueUtilityProcess({
"name": "DownloadProcess_" + cacheKey,
"processString": processString,
"onComplete": function(exitCode) {
if (exitCode !== 0) {
Logger.e("ImageCache", "Failed to download avatar for", username);
notifyCallbacks(cacheKey, "", false);
return ;
}
// Now process with ImageMagick
processCircularAvatar(tempPath, outputPath, cacheKey);
},
"onError": function() {
notifyCallbacks(cacheKey, "", false);
}
});
}
function processCircularAvatar(inputPath, outputPath, cacheKey) {
const srcEsc = inputPath.replace(/'/g, "'\\''");
const dstEsc = outputPath.replace(/'/g, "'\\''");
// ImageMagick command for circular crop with alpha
const command = `magick '${srcEsc}' -resize 256x256^ -gravity center -extent 256x256 -alpha set \\( +clone -channel A -evaluate set 0 +channel -fill white -draw 'circle 128,128 128,0' \\) -compose DstIn -composite '${dstEsc}'`;
queueImageMagickProcess({
"command": command,
"cacheKey": cacheKey,
"onComplete": function(exitCode) {
// Clean up temp file
Quickshell.execDetached(["rm", "-f", inputPath]);
if (exitCode !== 0) {
Logger.e("ImageCache", "Failed to create circular avatar");
notifyCallbacks(cacheKey, "", false);
} else {
Logger.d("ImageCache", "Circular avatar created:", outputPath);
notifyCallbacks(cacheKey, outputPath, true);
}
},
"onError": function() {
Quickshell.execDetached(["rm", "-f", inputPath]);
notifyCallbacks(cacheKey, "", false);
}
});
}
// Queue an ImageMagick process and run it when a slot is available // Queue an ImageMagick process and run it when a slot is available
function queueImageMagickProcess(request) { function queueImageMagickProcess(request) {
imageMagickQueue.push(request); imageMagickQueue.push(request);
@@ -616,13 +453,6 @@ Singleton {
// ------------------------------------------------- // -------------------------------------------------
// Cache Invalidation // Cache Invalidation
// ------------------------------------------------- // -------------------------------------------------
function invalidateThumbnail(sourcePath) {
Logger.i("ImageCache", "Invalidating thumbnail for:", sourcePath);
// Since cache keys include hash, we'd need to track mappings
// For simplicity, clear all thumbnails
clearThumbnails();
}
function invalidateLarge(sourcePath) { function invalidateLarge(sourcePath) {
Logger.i("ImageCache", "Invalidating large for:", sourcePath); Logger.i("ImageCache", "Invalidating large for:", sourcePath);
clearLarge(); clearLarge();
@@ -633,26 +463,13 @@ Singleton {
Quickshell.execDetached(["rm", "-f", path]); Quickshell.execDetached(["rm", "-f", path]);
} }
function invalidateAvatar(username) {
const path = contributorsDir + username + "_circular.png";
Quickshell.execDetached(["rm", "-f", path]);
}
// ------------------------------------------------- // -------------------------------------------------
// Clear Cache Functions // Clear Cache Functions
// ------------------------------------------------- // -------------------------------------------------
function clearAll() { function clearAll() {
Logger.i("ImageCache", "Clearing all cache"); Logger.i("ImageCache", "Clearing all cache");
clearThumbnails();
clearLarge(); clearLarge();
clearNotifications(); clearNotifications();
clearContributors();
}
function clearThumbnails() {
Logger.i("ImageCache", "Clearing thumbnails cache");
Quickshell.execDetached(["rm", "-rf", wpThumbDir]);
Quickshell.execDetached(["mkdir", "-p", wpThumbDir]);
} }
function clearLarge() { function clearLarge() {
@@ -667,82 +484,6 @@ Singleton {
Quickshell.execDetached(["mkdir", "-p", notificationsDir]); Quickshell.execDetached(["mkdir", "-p", notificationsDir]);
} }
function clearContributors() {
Logger.i("ImageCache", "Clearing contributors cache");
Quickshell.execDetached(["rm", "-rf", contributorsDir]);
Quickshell.execDetached(["mkdir", "-p", contributorsDir]);
}
// -------------------------------------------------
// Qt Fallback Renderer
// -------------------------------------------------
PanelWindow {
id: fallbackRenderer
implicitWidth: 0
implicitHeight: 0
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "noctalia-image-cache-renderer"
color: "transparent"
Image {
id: fallbackImage
property string cacheKey: ""
property string destPath: ""
property int targetSize: 256
function processNextFallback() {
cacheKey = "";
destPath = "";
source = "";
if (fallbackQueue.length > 0) {
const next = fallbackQueue.shift();
cacheKey = next.cacheKey;
destPath = next.destPath;
targetSize = next.size;
source = next.sourcePath;
} else {
fallbackProcessing = false;
}
}
width: targetSize
height: targetSize
visible: true
cache: false
asynchronous: true
fillMode: Image.PreserveAspectCrop
mipmap: true
antialiasing: true
onStatusChanged: {
if (!cacheKey)
return ;
if (status === Image.Ready) {
grabToImage(function(result) {
if (result.saveToFile(destPath)) {
Logger.d("ImageCache", "Fallback cache created:", destPath);
root.notifyCallbacks(cacheKey, destPath, true);
} else {
Logger.e("ImageCache", "Failed to save fallback cache");
root.notifyCallbacks(cacheKey, "", false);
}
processNextFallback();
});
} else if (status === Image.Error) {
Logger.e("ImageCache", "Fallback image load failed");
root.notifyCallbacks(cacheKey, "", false);
processNextFallback();
}
}
}
mask: Region {
}
}
// ------------------------------------------------- // -------------------------------------------------
// ImageMagick Detection // ImageMagick Detection
// ------------------------------------------------- // -------------------------------------------------
@@ -21,7 +21,7 @@ Singleton {
// line 2 <- current line // line 2 <- current line
// line 3 // line 3
property var lyrics: Array(linesCount).fill(" ") property var lyrics: Array(linesCount).fill(" ")
property bool showLyricsBar: ShellState.lyricsState.showLyricsBar || false readonly property bool showLyricsBar: ShellState.lyricsState.showLyricsBar || false
function toggleLyricsBar() { function toggleLyricsBar() {
ShellState.lyricsState = { ShellState.lyricsState = {
@@ -20,8 +20,8 @@ Singleton {
property string historyFile: Paths.cacheDir + "notifications.json" property string historyFile: Paths.cacheDir + "notifications.json"
// State // State
property real lastSeenTs: ShellState.notificationsState.lastSeenTs || 0 readonly property real lastSeenTs: ShellState.notificationsState.lastSeenTs || 0
property bool doNotDisturb: ShellState.notificationsState.doNotDisturb || false readonly property bool doNotDisturb: ShellState.notificationsState.doNotDisturb || false
// Models // Models
property ListModel activeList: ListModel {} property ListModel activeList: ListModel {}
@@ -11,7 +11,7 @@ Singleton {
property double _latitude: -1 property double _latitude: -1
property double _longitude: -1 property double _longitude: -1
property int temperature: 0 property int temperature: 0
property bool isEnabled: ShellState.sunsetState.enabled || false readonly property bool isEnabled: ShellState.sunsetState.enabled || false
function toggleSunset() { function toggleSunset() {
ShellState.sunsetState = { ShellState.sunsetState = {