diff --git a/.gitignore b/.gitignore
index e69de29b..2d6d8923 100644
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.qmlls.ini
+.zed
diff --git a/Assets/ColorScheme/Eldritch/Eldritch.json b/Assets/ColorScheme/Eldritch/Eldritch.json
new file mode 100644
index 00000000..3fedfeef
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/Eldritch.json
@@ -0,0 +1,34 @@
+{
+ "dark": {
+ "mPrimary": "#37f499",
+ "mOnPrimary": "#171928",
+ "mSecondary": "#04d1f9",
+ "mOnSecondary": "#171928",
+ "mTertiary": "#a48cf2",
+ "mOnTertiary": "#171928",
+ "mError": "#f16c75",
+ "mOnError": "#171928",
+ "mSurface": "#212337",
+ "mOnSurface": "#ebfafa",
+ "mSurfaceVariant": "#292e42",
+ "mOnSurfaceVariant": "#ABB4DA",
+ "mOutline": "#3b4261",
+ "mShadow": "#414868"
+ },
+ "light": {
+ "mPrimary": "#37f499",
+ "mOnPrimary": "#171928",
+ "mSecondary": "#04d1f9",
+ "mOnSecondary": "#171928",
+ "mTertiary": "#a48cf2",
+ "mOnTertiary": "#171928",
+ "mError": "#f16c75",
+ "mOnError": "#171928",
+ "mSurface": "#ffffff",
+ "mOnSurface": "#171928",
+ "mSurfaceVariant": "#f2f4f8",
+ "mOnSurfaceVariant": "#3b4261",
+ "mOutline": "#b0b6c3",
+ "mShadow": "#e0e3e8"
+ }
+}
diff --git a/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-dark b/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-dark
new file mode 100644
index 00000000..dc0c5b58
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-dark
@@ -0,0 +1,23 @@
+[colors]
+foreground=ebfafa
+background=212337
+regular0=21222c
+regular1=f9515d
+regular2=37f499
+regular3=e9f941
+regular4=9071f4
+regular5=f265b5
+regular6=04d1f9
+regular7=ebfafa
+bright0=7081d0
+bright1=f16c75
+bright2=69F8B3
+bright3=f1fc79
+bright4=a48cf2
+bright5=FD92CE
+bright6=66e4fd
+bright7=ffffff
+selection-foreground=ebfafa
+selection-background=bf4f8e
+cursor=37f499 f8f8f2
+
diff --git a/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-light b/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-light
new file mode 100644
index 00000000..90602968
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-light
@@ -0,0 +1,22 @@
+[colors]
+foreground=212337
+background=ebfafa
+regular0=ebfafa # black → white
+regular1=ba1a1a # red (darker)
+regular2=1a7f4c # green (darker)
+regular3=9e8c13 # yellow (darker)
+regular4=3a3e8c # blue (darker)
+regular5=8c2a6c # magenta (darker)
+regular6=1a6c8c # cyan (darker)
+regular7=212337 # white → black
+bright0=7081d0 # keep as accent
+bright1=d23b3b # lighter red
+bright2=37f499 # original green
+bright3=e9f941 # original yellow
+bright4=9071f4 # original blue
+bright5=f265b5 # original magenta
+bright6=04d1f9 # original cyan
+bright7=212337 # black
+selection-foreground=212337
+selection-background=bf4f8e
+cursor=1a7f4c 212337
diff --git a/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-dark b/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-dark
new file mode 100644
index 00000000..e6b33937
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-dark
@@ -0,0 +1,21 @@
+palette = 0=#21222c
+palette = 1=#f9515d
+palette = 2=#37f499
+palette = 3=#e9f941
+palette = 4=#9071f4
+palette = 5=#f265b5
+palette = 6=#04d1f9
+palette = 7=#ebfafa
+palette = 8=#7081d0
+palette = 9=#f16c75
+palette = 10=#69F8B3
+palette = 11=#f1fc79
+palette = 12=#a48cf2
+palette = 13=#FD92CE
+palette = 14=#66e4fd
+palette = 15=#ffffff
+background = 212337
+foreground = ebfafa
+cursor-color = 37f499
+selection-background = bf4f8e
+selection-foreground = ebfafa
diff --git a/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-light b/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-light
new file mode 100644
index 00000000..24b6cba5
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-light
@@ -0,0 +1,22 @@
+palette = 0=#f8f8fa # base background (was dark, now light)
+palette = 1=#d7263d # red (less saturated)
+palette = 2=#1eb980 # green (cooler, lighter)
+palette = 3=#f7c948 # yellow (warmer)
+palette = 4=#5e60ce # blue (lighter)
+palette = 5=#c77dff # magenta (lighter)
+palette = 6=#38a1db # cyan (lighter)
+palette = 7=#21222c # base foreground (was background, now dark)
+palette = 8=#bfc9e3 # bright black (light gray)
+palette = 9=#f9515d # bright red
+palette = 10=#37f499 # bright green
+palette = 11=#e9f941 # bright yellow
+palette = 12=#9071f4 # bright blue
+palette = 13=#f265b5 # bright magenta
+palette = 14=#04d1f9 # bright cyan
+palette = 15=#21222c # bright white (dark for contrast)
+background = f8f8fa
+foreground = 21222c
+cursor-color = 5e60ce
+selection-background = d7e3fa
+selection-foreground = 21222c
+
diff --git a/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-dark.conf b/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-dark.conf
new file mode 100644
index 00000000..8decd1c9
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-dark.conf
@@ -0,0 +1,22 @@
+color0 #21222c
+color1 #f9515d
+color2 #37f499
+color3 #e9f941
+color4 #9071f4
+color5 #f265b5
+color6 #04d1f9
+color7 #ebfafa
+color8 #7081d0
+color9 #f16c75
+color10 #69F8B3
+color11 #f1fc79
+color12 #a48cf2
+color13 #FD92CE
+color14 #66e4fd
+color15 #ffffff
+background #212337
+selection_foreground #ebfafa
+cursor #37f499
+cursor_text_color #f8f8f2
+foreground #ebfafa
+selection_background #bf4f8e
diff --git a/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-light.conf b/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-light.conf
new file mode 100644
index 00000000..eb75e925
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-light.conf
@@ -0,0 +1,22 @@
+color0 #ebfafa
+color1 #f9515d
+color2 #37f499
+color3 #e9f941
+color4 #9071f4
+color5 #f265b5
+color6 #04d1f9
+color7 #212337
+color8 #7081d0
+color9 #f16c75
+color10 #69F8B3
+color11 #f1fc79
+color12 #a48cf2
+color13 #FD92CE
+color14 #66e4fd
+color15 #323449
+background #ebfafa
+selection_foreground #ebfafa
+cursor #212337
+cursor_text_color #ebfafa
+foreground #212337
+selection_background #bf4f8e
diff --git a/Assets/ColorScheme/Rosepine/Rosepine.json b/Assets/ColorScheme/Rosepine/Rosepine.json
index 5e36ba0a..6b6f2f52 100644
--- a/Assets/ColorScheme/Rosepine/Rosepine.json
+++ b/Assets/ColorScheme/Rosepine/Rosepine.json
@@ -8,7 +8,7 @@
"mOnTertiary": "#e0def4",
"mError": "#eb6f92",
"mOnError": "#191724",
- "mSurface": "#191724",
+ "mSurface": "#1f1d2e",
"mOnSurface": "#e0def4",
"mSurfaceVariant": "#26233a",
"mOnSurfaceVariant": "#908caa",
@@ -24,7 +24,7 @@
"mOnTertiary": "#575279",
"mError": "#b4637a",
"mOnError": "#faf4ed",
- "mSurface": "#faf4ed",
+ "mSurface": "#fffaf3",
"mOnSurface": "#575279",
"mSurfaceVariant": "#f2e9e1",
"mOnSurfaceVariant": "#797593",
diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json
index 6a5bb0bd..a3318920 100644
--- a/Assets/Translations/de.json
+++ b/Assets/Translations/de.json
@@ -32,6 +32,16 @@
"reset": "Eckenradius des Bildschirms zurücksetzen"
}
},
+ "lockscreen": {
+ "section": {
+ "label": "Sperrbildschirm",
+ "description": "Sperrbildschirm-Verhalten konfigurieren."
+ },
+ "lock-on-suspend": {
+ "label": "Beim Standby sperren",
+ "description": "Bildschirm automatisch sperren, wenn das System in den Standby-Modus wechselt."
+ }
+ },
"fonts": {
"section": {
"label": "Schriftarten",
@@ -58,6 +68,17 @@
}
},
"reset-scaling": "Skalierung zurücksetzen"
+ },
+ "language": {
+ "section": {
+ "label": "Sprache",
+ "description": "Wählen Sie Ihre bevorzugte Sprache für die Anwendung."
+ },
+ "select": {
+ "label": "Anwendungssprache",
+ "description": "Wählen Sie die in der Anwendungsoberfläche verwendete Sprache.",
+ "auto-detect": "Automatisch"
+ }
}
},
"audio": {
@@ -261,6 +282,10 @@
"label": "Dock-Schwebeabstand",
"description": "Schwebeabstand vom Bildschirmrand anpassen."
},
+ "icon-size": {
+ "label": "Dock-Größe",
+ "description": "Gesamtgröße des Docks anpassen."
+ },
"colorize-icons": {
"label": "Symbole einfärben",
"description": "Theme-Farben auf Dock-App-Symbole anwenden (nur nicht fokussierte Apps)."
@@ -475,10 +500,6 @@
"label": "Farbquelle",
"description": "Haupteinstellungen für Noctalias Farben."
},
- "dark-mode": {
- "label": "Dunkler Modus",
- "description": "Wechselt zu einem dunkleren Theme für einfachere Betrachtung bei Nacht."
- },
"use-wallpaper-colors": {
"label": "Hintergrundbild-Farben verwenden",
"description": "Farbschemata aus Ihrem Hintergrundbild mit Matugen generieren. Extrahiert automatisch Farben für ein kohärentes Design."
@@ -488,6 +509,19 @@
"description": "Wähle einen Farbstil für Matugen aus."
}
},
+ "dark-mode": {
+ "switch": {
+ "label": "Dunkler Modus",
+ "description": "Wechselt zu einem dunkleren Theme für einfachere Betrachtung bei Nacht."
+ },
+ "mode": {
+ "label": "Automatischer dunkler Modus",
+ "description": "Ermöglicht automatisches Wechseln zwischen dem hellen und dunklen Modus.",
+ "off": "Aus",
+ "manual": "Manuell",
+ "location": "Standort"
+ }
+ },
"predefined": {
"section": {
"label": "Vordefinierte Farbschemata",
@@ -537,7 +571,7 @@
},
"discord": {
"description": "Schreibt {filepath} für {client}",
- "description-missing": "Kein Discord-Client erkannt. Installieren Sie vesktop, webcord, armcord, equibop, lightcord oder dorion."
+ "description-missing": "Kein Discord-Client erkannt. Installieren Sie vencord, vesktop, webcord, armcord, equibop, lightcord oder dorion."
},
"pywalfox": {
"description": "Schreibt {filepath} und führt pywalfox update aus",
@@ -549,7 +583,7 @@
"description": "Zusätzliche Konfigurationsoptionen.",
"user-templates": {
"label": "Benutzer-Vorlagen",
- "description": "Benutzerdefinierte Matugen-Konfiguration aus ~/.config/matugen/config.toml aktivieren"
+ "description": "Benutzerdefinierte Matugen-Konfiguration aktivieren. Eine Vorlagendatei wird beim ersten Aktivieren unter ~/.config/noctalia/user-templates.toml erstellt"
}
},
"section": {
@@ -576,6 +610,10 @@
"label": "Wetter",
"description": "Bevorzugte Temperatureinheit wählen."
},
+ "enabled": {
+ "label": "Wetter aktivieren",
+ "description": "Wetterinformationen in der gesamten Benutzeroberfläche anzeigen und Wetterdaten abrufen. Wenn deaktiviert, werden alle Wetterelemente ausgeblendet und es werden keine Netzwerkanfragen gestellt."
+ },
"fahrenheit": {
"label": "Temperatur in Fahrenheit (°F) anzeigen",
"description": "Temperatur in Fahrenheit statt Celsius anzeigen."
@@ -742,7 +780,7 @@
"label": "Widgets für Kurzbefehle"
},
"sectionLeft": "Links",
- "sectionRight": "Richtig"
+ "sectionRight": "Rechts"
}
},
"user-interface": {
@@ -914,9 +952,13 @@
"label": "Scrollmodus",
"description": "Steuern, wann Textscrolling für lange Fenstertitel aktiviert ist."
},
- "width": {
- "description": "Steuert die horizontale Größe des Widgets.",
- "label": "Widget-Breite"
+ "max-width": {
+ "label": "Maximale Breite",
+ "description": "Stellt die maximale Horizontalgröße des Widgets ein. Das Widget wird sich an kürzere Inhalte anpassen."
+ },
+ "use-fixed-width": {
+ "label": "Feste Breite verwenden",
+ "description": "Wenn aktiviert, verwendet das Widget immer die maximale Breite, anstatt sich dynamisch an den Inhalt anzupassen."
},
"colorize-icons": {
"label": "Symbole einfärben",
@@ -1067,6 +1109,14 @@
"label": "Visualizer-Typ",
"description": "Stil des Audio-Visualizers auswählen."
},
+ "max-width": {
+ "label": "Maximale Breite",
+ "description": "Stellt die maximale Horizontalgröße des Widgets ein. Das Widget wird sich an kürzere Inhalte anpassen."
+ },
+ "use-fixed-width": {
+ "label": "Feste Breite verwenden",
+ "description": "Wenn aktiviert, verwendet das Widget immer die maximale Breite, anstatt sich dynamisch an den Inhalt anzupassen."
+ },
"scrolling-mode": {
"label": "Scrollmodus",
"description": "Steuern, wann Textscrolling für lange Track-Titel aktiviert ist."
@@ -1088,7 +1138,7 @@
},
"horizontal-bar": {
"label": "Horizontale Statusleiste",
- "description": "Tipp: Verwenden Sie \\n, um einen Zeilenumbruch zu erstellen."
+ "description": "Tipp: Verwenden Sie \n, um einen Zeilenumbruch zu erstellen."
},
"vertical-bar": {
"label": "Vertikale Statusleiste",
@@ -1203,6 +1253,9 @@
"calendar": {
"panel": {
"week": "Woche"
+ },
+ "weather": {
+ "loading": "Wetter wird geladen…"
}
},
"tooltips": {
@@ -1230,26 +1283,26 @@
"keep-awake": "Wach bleiben",
"enable-keep-awake": "Wach bleiben aktivieren",
"disable-keep-awake": "Wach bleiben deaktivieren",
- "wallpaper-selector": "Linksklick: Hintergrundbild-Auswahl öffnen.\\nRechtsklick: Zufälliges Hintergrundbild setzen.",
+ "wallpaper-selector": "Linksklick: Hintergrundbild-Auswahl öffnen.\nRechtsklick: Zufälliges Hintergrundbild setzen.",
"do-not-disturb-enabled": "'Nicht stören' aktiviert",
"do-not-disturb-disabled": "'Nicht stören' deaktiviert",
"connect-disconnect-devices": "Linksklick zum Verbinden. Rechtsklick zum Vergessen.",
"set-power-profile": "'{profile}' Energieprofil setzen",
"switch-to-light-mode": "Zum hellen Modus wechseln",
"switch-to-dark-mode": "Zum dunklen Modus wechseln",
- "night-light-disabled": "Nachtlicht ist deaktiviert.\\nLinksklick zum Wechseln des Modus.\\nRechtsklick für Einstellungen.",
- "night-light-enabled": "Nachtlicht ist aktiviert.\\nLinksklick zum Wechseln des Modus.\\nRechtsklick für Einstellungen.",
- "night-light-forced": "Nachtlicht ist erzwungen.\\nLinksklick zum Wechseln des Modus.\\nRechtsklick für Einstellungen.",
- "night-light-not-installed": "Nachtlicht ist nicht verfügbar.\\nwlsunset ist nicht installiert.",
+ "night-light-disabled": "Nachtlicht ist deaktiviert.\nLinksklick zum Wechseln des Modus.\nRechtsklick für Einstellungen.",
+ "night-light-enabled": "Nachtlicht ist aktiviert.\nLinksklick zum Wechseln des Modus.\nRechtsklick für Einstellungen.",
+ "night-light-forced": "Nachtlicht ist erzwungen.\nLinksklick zum Wechseln des Modus.\nRechtsklick für Einstellungen.",
+ "night-light-not-installed": "Nachtlicht ist nicht verfügbar.\nwlsunset ist nicht installiert.",
"click-to-start-recording": "Klicken zum Starten der Aufnahme",
"click-to-stop-recording": "Klicken zum Stoppen der Aufnahme",
"open-control-center": "Kontrollzentrum öffnen",
- "volume-at": "Lautstärke bei {volume}%\\nLinksklick zum Stumm-/Lautschalten. Rechtsklick für Einstellungen.\\nScrollen zum Ändern der Lautstärke.",
- "microphone-volume-at": "Mikrofon-Lautstärke bei {volume}%\\nLinksklick zum Stumm-/Lautschalten. Rechtsklick für Einstellungen.\\nScrollen zum Ändern der Lautstärke.",
+ "volume-at": "Lautstärke bei {volume}%\nLinksklick zum Stumm-/Lautschalten. Rechtsklick für Einstellungen.\nScrollen zum Ändern der Lautstärke.",
+ "microphone-volume-at": "Mikrofon-Lautstärke bei {volume}%\nLinksklick zum Stumm-/Lautschalten. Rechtsklick für Einstellungen.\nScrollen zum Ändern der Lautstärke.",
"manage-wifi": "WLAN verwalten",
"bluetooth-devices": "Bluetooth-Geräte",
- "open-notification-history-enable-dnd": "Benachrichtigungsverlauf öffnen\\nRechtsklick um 'Nicht stören' zu aktivieren.",
- "open-notification-history-disable-dnd": "Benachrichtigungsverlauf öffnen\\nRechtsklick um 'Nicht stören' zu deaktivieren.",
+ "open-notification-history-enable-dnd": "Benachrichtigungsverlauf öffnen\nRechtsklick um 'Nicht stören' zu aktivieren.",
+ "open-notification-history-disable-dnd": "Benachrichtigungsverlauf öffnen\nRechtsklick um 'Nicht stören' zu deaktivieren.",
"open-wallpaper-selector": "Hintergrundbild-Auswahl öffnen",
"previous-media": "Vorheriges Medium",
"pause": "Pausieren",
@@ -1322,7 +1375,8 @@
"bottom_left": "Unten links",
"bottom_right": "Unten rechts",
"bottom_center": "Unten mittig",
- "top_center": "Oben mittig"
+ "top_center": "Oben mittig",
+ "center": "Mitte"
},
"quickSettingsStyle": {
"modern": "Modern",
@@ -1444,6 +1498,11 @@
"discharging-rate": "Entladerate: {rate} W.",
"charging": "Wird geladen.",
"discharging": "Wird entladen.",
+ "plugged-in": "Angeschlossen.",
+ "idle": "Leerlauf.",
+ "time-left": "Verbleibende Zeit: {time}.",
+ "time-until-full": "Zeit bis vollständig geladen: {time}.",
+ "health": "Zustand: {percent}%",
"panel": {
"title": "Ladeschwelle",
"full": "Volle Kapazität ({percent}%)",
@@ -1622,5 +1681,34 @@
"label": "Letztes Control-Center-Widget entfernt",
"description": "Das Control-Center-Widget wurde aus der Leiste entfernt. Um es erneut über die Leiste zu öffnen, fügen Sie das Widget wieder hinzu. Sie können es auch durch Rechtsklick auf die Leiste öffnen."
}
+ },
+ "setup": {
+ "customize": {
+ "header": "Erlebnis anpassen",
+ "subheader": "Leistenposition, Dichte, Skalierung und mehr einstellen."
+ },
+ "appearance": {
+ "header": "Erscheinungsbild",
+ "subheader": "Dunkelmodus und Farbquellen wählen (Matugen oder vordefiniert)."
+ },
+ "wallpaper": {
+ "header": "Wähle dein Hintergrundbild",
+ "subheader": "Bestimme die Stimmung mit einem schönen Hintergrund.",
+ "select-prompt": "Wähle unten ein Hintergrundbild",
+ "preview-error": "Bild konnte nicht geladen werden",
+ "none-in-dir": "Keine Hintergrundbilder im Verzeichnis gefunden",
+ "no-dir": "Kein Hintergrundbild-Verzeichnis ausgewählt",
+ "no-valid": "Keine gültigen Bilddateien gefunden in: {dir}",
+ "choose-dir": "Wähle ein Verzeichnis mit deinen Hintergrundbildern",
+ "dir": {
+ "label": "Hintergrundbild-Verzeichnis",
+ "description": "Wähle den Ordner mit deinen Hintergrundbildern",
+ "browse": "Ordner auswählen",
+ "select-title": "Hintergrundbild-Ordner wählen"
+ }
+ },
+ "welcome": {
+ "note": "Nur ein paar Grundeinstellungen – alle Optionen findest du in den Einstellungen"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json
index 7f6cf5c7..cbad3dd8 100644
--- a/Assets/Translations/en.json
+++ b/Assets/Translations/en.json
@@ -32,6 +32,16 @@
"reset": "Reset screen corners radius"
}
},
+ "lockscreen": {
+ "section": {
+ "label": "Lock screen",
+ "description": "Configure lock screen behavior."
+ },
+ "lock-on-suspend": {
+ "label": "Lock on suspend",
+ "description": "Automatically lock the screen when suspending the system."
+ }
+ },
"fonts": {
"reset-scaling": "Reset scaling",
"section": {
@@ -58,6 +68,17 @@
"description": "Increase or decrease the size of the monospaced text."
}
}
+ },
+ "language": {
+ "section": {
+ "label": "Language",
+ "description": "Choose your preferred language for the application."
+ },
+ "select": {
+ "label": "Application Language",
+ "description": "Select the language used in the application's interface.",
+ "auto-detect": "Automatic"
+ }
}
},
"audio": {
@@ -259,7 +280,11 @@
},
"floating-distance": {
"label": "Dock floating distance",
- "description": "Adjust the floating distance from the screen edge."
+ "description": "Set the distance between the dock and the edge of the screen."
+ },
+ "icon-size": {
+ "label": "Dock size",
+ "description": "Adjust the overall size of the dock."
},
"colorize-icons": {
"label": "Colorize Icons",
@@ -475,10 +500,6 @@
"label": "Color source",
"description": "Main settings for Noctalia's colors."
},
- "dark-mode": {
- "label": "Dark mode",
- "description": "Switches to a darker theme for easier viewing at night."
- },
"use-wallpaper-colors": {
"label": "Use wallpaper colors",
"description": "Generate color schemes from your wallpaper using Matugen. Automatically extracts colors to create a cohesive theme."
@@ -488,6 +509,19 @@
"description": "Choose the color scheme generation algorithm for Matugen."
}
},
+ "dark-mode": {
+ "switch": {
+ "label": "Dark mode",
+ "description": "Switches to a darker theme for easier viewing at night."
+ },
+ "mode": {
+ "label": "Dark mode schedule",
+ "description": "Enables automatic switching between light and dark mode.",
+ "off": "Off",
+ "manual": "Manual",
+ "location": "Location"
+ }
+ },
"predefined": {
"section": {
"label": "Predefined color schemes",
@@ -541,7 +575,7 @@
},
"discord": {
"description": "Write {filepath} for {client}",
- "description-missing": "No Discord client detected. Install vesktop, webcord, armcord, equibop, lightcord, or dorion."
+ "description-missing": "No Discord client detected. Install vencord, vesktop, webcord, armcord, equibop, lightcord, or dorion."
},
"pywalfox": {
"description": "Write {filepath} and run pywalfox update",
@@ -553,7 +587,7 @@
"description": "Additional configuration options.",
"user-templates": {
"label": "User templates",
- "description": "Enable user-defined Matugen config from ~/.config/matugen/config.toml"
+ "description": "Enable user-defined Matugen config. A template file will be created at ~/.config/noctalia/user-templates.toml on first enable"
}
}
}
@@ -576,6 +610,10 @@
"label": "Weather",
"description": "Choose your preferred temperature unit."
},
+ "enabled": {
+ "label": "Enable weather",
+ "description": "Show weather information throughout the interface and fetch weather data. When disabled, all weather elements will be hidden and no network requests will be made."
+ },
"fahrenheit": {
"label": "Display temperature in Fahrenheit (°F)",
"description": "Display temperature in Fahrenheit instead of Celsius."
@@ -897,9 +935,13 @@
"label": "Scrolling mode",
"description": "Control when text scrolling is enabled for long window titles."
},
- "width": {
- "label": "Widget Width",
- "description": "Controls the horizontal size of the widget."
+ "max-width": {
+ "label": "Maximum Width",
+ "description": "Sets the maximum horizontal size of the widget. The widget will shrink to fit shorter content."
+ },
+ "use-fixed-width": {
+ "label": "Use Fixed Width",
+ "description": "When enabled, the widget will always use the maximum width instead of dynamically adjusting to content."
},
"colorize-icons": {
"label": "Colorize Icons",
@@ -1050,6 +1092,14 @@
"label": "Visualizer type",
"description": "Choose the style of audio visualizer to display."
},
+ "max-width": {
+ "label": "Maximum Width",
+ "description": "Sets the maximum horizontal size of the widget. The widget will shrink to fit shorter content."
+ },
+ "use-fixed-width": {
+ "label": "Use Fixed Width",
+ "description": "When enabled, the widget will always use the maximum width instead of dynamically adjusting to content."
+ },
"scrolling-mode": {
"label": "Scrolling mode",
"description": "Control when text scrolling is enabled for long track titles."
@@ -1183,11 +1233,7 @@
"scan-again": "Scan again"
}
},
- "calendar": {
- "panel": {
- "week": "Week"
- }
- },
+
"tooltips": {
"refresh": "Refresh",
"close": "Close",
@@ -1244,6 +1290,14 @@
"clock": {
"tooltip": "Open calendar"
},
+ "calendar": {
+ "panel": {
+ "week": "Week"
+ },
+ "weather": {
+ "loading": "Loading weather…"
+ }
+ },
"dock": {
"menu": {
"focus": "Focus",
@@ -1305,7 +1359,8 @@
"bottom_left": "Bottom left",
"bottom_right": "Bottom right",
"bottom_center": "Bottom center",
- "top_center": "Top center"
+ "top_center": "Top center",
+ "center": "Center"
},
"quickSettingsStyle": {
"modern": "Modern",
@@ -1615,6 +1670,11 @@
"discharging-rate": "Discharging rate: {rate} W.",
"charging": "Charging.",
"discharging": "Discharging.",
+ "plugged-in": "Plugged in.",
+ "idle": "Idle.",
+ "time-left": "Time left: {time}.",
+ "time-until-full": "Time until full: {time}.",
+ "health": "Health: {percent}%",
"panel": {
"title": "Charge threshold",
"full": "Full capacity ({percent}%)",
@@ -1622,5 +1682,34 @@
"lifespan": "Extended lifespan ({percent}%)",
"disabled": "Battery manager disabled"
}
+ },
+ "setup": {
+ "customize": {
+ "header": "Customize your experience",
+ "subheader": "Adjust bar position, density, scaling and more."
+ },
+ "appearance": {
+ "header": "Appearance",
+ "subheader": "Choose dark mode and color sources (Matugen or predefined)."
+ },
+ "wallpaper": {
+ "header": "Choose your wallpaper",
+ "subheader": "Set the mood with a beautiful background.",
+ "select-prompt": "Select a wallpaper below",
+ "preview-error": "Failed to load image",
+ "none-in-dir": "No wallpapers found in directory",
+ "no-dir": "No wallpaper directory selected",
+ "no-valid": "No valid image files found in: {dir}",
+ "choose-dir": "Choose a directory containing your wallpaper images",
+ "dir": {
+ "label": "Wallpaper directory",
+ "description": "Choose the folder containing your wallpapers",
+ "browse": "Browse for wallpaper folder",
+ "select-title": "Select wallpaper folder"
+ }
+ },
+ "welcome": {
+ "note": "Just a few basics to get you started - full options are in Settings"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json
index 9ced10d2..2db6da8f 100644
--- a/Assets/Translations/es.json
+++ b/Assets/Translations/es.json
@@ -32,6 +32,16 @@
"reset": "Restablecer el radio de las esquinas de la pantalla"
}
},
+ "lockscreen": {
+ "section": {
+ "label": "Pantalla de bloqueo",
+ "description": "Configura el comportamiento de la pantalla de bloqueo."
+ },
+ "lock-on-suspend": {
+ "label": "Bloquear al suspender",
+ "description": "Bloquear automáticamente la pantalla al suspender el sistema."
+ }
+ },
"fonts": {
"section": {
"label": "Fuentes",
@@ -58,6 +68,17 @@
}
},
"reset-scaling": "Restablecer la escala"
+ },
+ "language": {
+ "section": {
+ "label": "Idioma",
+ "description": "Elige tu idioma preferido para la aplicación."
+ },
+ "select": {
+ "label": "Idioma de la aplicación",
+ "description": "Selecciona el idioma utilizado en la interfaz de la aplicación.",
+ "auto-detect": "Automático"
+ }
}
},
"audio": {
@@ -261,6 +282,10 @@
"label": "Distancia de flotación del dock",
"description": "Ajusta la distancia de flotación desde el borde de la pantalla."
},
+ "icon-size": {
+ "label": "Tamaño del Dock",
+ "description": "Ajusta el tamaño general del Dock."
+ },
"colorize-icons": {
"label": "Colorear iconos",
"description": "Aplicar colores del tema a los iconos de aplicaciones del dock (solo aplicaciones no enfocadas)."
@@ -475,10 +500,6 @@
"label": "Fuente de color",
"description": "Configuración principal de los colores de Noctalia."
},
- "dark-mode": {
- "label": "Modo oscuro",
- "description": "Cambia a un tema más oscuro para una visualización más fácil por la noche."
- },
"use-wallpaper-colors": {
"label": "Usar colores del fondo de pantalla",
"description": "Generar esquemas de color desde tu fondo de pantalla usando Matugen. Extrae automáticamente colores para crear un tema cohesivo."
@@ -488,6 +509,12 @@
"description": "Elige el algoritmo de generación de esquema de colores para Matugen."
}
},
+ "dark-mode": {
+ "switch": {
+ "label": "Modo oscuro",
+ "description": "Cambia a un tema más oscuro para una visualización más fácil por la noche."
+ }
+ },
"predefined": {
"section": {
"label": "Esquemas de colores predefinidos",
@@ -537,7 +564,7 @@
},
"discord": {
"description": "Escribir {filepath} para {client}",
- "description-missing": "No se detectó cliente de Discord. Instala vesktop, webcord, armcord, equibop, lightcord o dorion."
+ "description-missing": "No se detectó cliente de Discord. Instala vencord, vesktop, webcord, armcord, equibop, lightcord o dorion."
},
"pywalfox": {
"description": "Escribir {filepath} y ejecutar pywalfox update",
@@ -549,7 +576,7 @@
"description": "Opciones de configuración adicionales.",
"user-templates": {
"label": "Plantillas de usuario",
- "description": "Habilitar configuración de Matugen definida por el usuario desde ~/.config/matugen/config.toml"
+ "description": "Habilitar configuración de Matugen definida por el usuario. Se creará un archivo de plantilla en ~/.config/noctalia/user-templates.toml al activar por primera vez"
}
},
"section": {
@@ -576,6 +603,10 @@
"label": "Clima",
"description": "Elige tu unidad de temperatura preferida."
},
+ "enabled": {
+ "label": "Activar clima",
+ "description": "Mostrar información del clima en toda la interfaz y obtener datos meteorológicos. Cuando está desactivado, todos los elementos del clima estarán ocultos y no se realizarán solicitudes de red."
+ },
"fahrenheit": {
"label": "Mostrar temperatura en Fahrenheit (°F)",
"description": "Muestra la temperatura en Fahrenheit en lugar de Celsius."
@@ -897,9 +928,13 @@
"label": "Modo de desplazamiento",
"description": "Controlar cuándo está habilitado el desplazamiento de texto para títulos de ventana largos."
},
- "width": {
- "description": "Controla el tamaño horizontal del widget.",
- "label": "Ancho del widget"
+ "max-width": {
+ "label": "Ancho Máximo",
+ "description": "Establece el tamaño horizontal máximo del widget. El widget se reducirá para adaptarse a contenido más corto."
+ },
+ "use-fixed-width": {
+ "label": "Usar Ancho Fijo",
+ "description": "Cuando está activado, el widget siempre usará el ancho máximo en lugar de ajustarse dinámicamente al contenido."
},
"colorize-icons": {
"label": "Colorear iconos",
@@ -1050,6 +1085,14 @@
"label": "Tipo de visualizador",
"description": "Elegir el estilo de visualizador de audio a mostrar."
},
+ "max-width": {
+ "label": "Ancho Máximo",
+ "description": "Establece el tamaño horizontal máximo del widget. El widget se reducirá para adaptarse a contenido más corto."
+ },
+ "use-fixed-width": {
+ "label": "Usar Ancho Fijo",
+ "description": "Cuando está activado, el widget siempre usará el ancho máximo en lugar de ajustarse dinámicamente al contenido."
+ },
"scrolling-mode": {
"label": "Modo de desplazamiento",
"description": "Controlar cuándo está habilitado el desplazamiento de texto para títulos de pista largos."
@@ -1186,6 +1229,9 @@
"calendar": {
"panel": {
"week": "Semana"
+ },
+ "weather": {
+ "loading": "Cargando el clima…"
}
},
"tooltips": {
@@ -1305,7 +1351,8 @@
"bottom_left": "Inferior izquierda",
"bottom_right": "Inferior derecha",
"bottom_center": "Inferior central",
- "top_center": "Superior central"
+ "top_center": "Superior central",
+ "center": "Centro"
},
"quickSettingsStyle": {
"modern": "Moderno",
@@ -1615,6 +1662,11 @@
"discharging-rate": "Tasa de descarga: {rate} W.",
"charging": "Cargando.",
"discharging": "Descargando.",
+ "plugged-in": "Conectado.",
+ "idle": "Inactivo.",
+ "time-left": "Tiempo restante: {time}.",
+ "time-until-full": "Tiempo hasta carga completa: {time}.",
+ "health": "Salud: {percent}%",
"panel": {
"title": "Umbral de carga",
"full": "Capacidad total ({percent}%)",
@@ -1622,5 +1674,34 @@
"lifespan": "Vida útil prolongada ({percent}%)",
"disabled": "Administrador de batería deshabilitado"
}
+ },
+ "setup": {
+ "customize": {
+ "header": "Personaliza tu experiencia",
+ "subheader": "Ajusta la posición de la barra, densidad, escala y más."
+ },
+ "appearance": {
+ "header": "Apariencia",
+ "subheader": "Elige modo oscuro y fuentes de color (Matugen o predefinido)."
+ },
+ "wallpaper": {
+ "header": "Elige tu fondo",
+ "subheader": "Define el ambiente con un bonito fondo.",
+ "select-prompt": "Selecciona un fondo abajo",
+ "preview-error": "No se pudo cargar la imagen",
+ "none-in-dir": "No se encontraron fondos en el directorio",
+ "no-dir": "No se seleccionó un directorio de fondos",
+ "no-valid": "No se encontraron imágenes válidas en: {dir}",
+ "choose-dir": "Elige un directorio que contenga tus fondos",
+ "dir": {
+ "label": "Directorio de fondos",
+ "description": "Elige la carpeta que contiene tus fondos",
+ "browse": "Seleccionar carpeta",
+ "select-title": "Seleccionar carpeta de fondos"
+ }
+ },
+ "welcome": {
+ "note": "Solo algunos conceptos básicos para empezar: todas las opciones están en Configuración."
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json
index 1d2d9568..8131592a 100644
--- a/Assets/Translations/fr.json
+++ b/Assets/Translations/fr.json
@@ -32,6 +32,16 @@
"reset": "Réinitialiser le rayon des coins de l'écran"
}
},
+ "lockscreen": {
+ "section": {
+ "label": "Écran de verrouillage",
+ "description": "Configurer le comportement de l'écran de verrouillage."
+ },
+ "lock-on-suspend": {
+ "label": "Verrouiller lors de la suspension",
+ "description": "Verrouiller automatiquement l'écran lors de la mise en veille du système."
+ }
+ },
"fonts": {
"section": {
"label": "Polices",
@@ -58,6 +68,17 @@
}
},
"reset-scaling": "Réinitialiser l'échelle"
+ },
+ "language": {
+ "section": {
+ "label": "Langue",
+ "description": "Choisissez votre langue préférée pour l'application."
+ },
+ "select": {
+ "label": "Langue de l'application",
+ "description": "Sélectionnez la langue utilisée dans l'interface de l'application.",
+ "auto-detect": "Automatique"
+ }
}
},
"audio": {
@@ -261,6 +282,10 @@
"label": "Distance de flottaison du dock",
"description": "Ajustez la distance de flottaison par rapport au bord de l'écran."
},
+ "icon-size": {
+ "label": "Taille du Dock",
+ "description": "Ajuster la taille globale du Dock."
+ },
"colorize-icons": {
"label": "Coloriser les icônes",
"description": "Appliquer les couleurs du thème aux icônes d'applications du dock (applications non focalisées uniquement)."
@@ -475,10 +500,6 @@
"label": "Source des couleurs",
"description": "Paramètres principaux pour les couleurs de Noctalia."
},
- "dark-mode": {
- "label": "Mode sombre",
- "description": "Passe à un thème plus sombre pour une visualisation plus facile la nuit."
- },
"use-wallpaper-colors": {
"label": "Utiliser les couleurs du fond d'écran",
"description": "Générer des schémas de couleurs à partir de votre fond d'écran avec Matugen. Extrait automatiquement les couleurs pour créer un thème cohérent."
@@ -488,6 +509,12 @@
"description": "Choisissez l'algorithme de génération de schéma de couleurs pour Matugen."
}
},
+ "dark-mode": {
+ "switch": {
+ "label": "Mode sombre",
+ "description": "Passe à un thème plus sombre pour une visualisation plus facile la nuit."
+ }
+ },
"predefined": {
"section": {
"label": "Jeux de couleurs prédéfinis",
@@ -537,7 +564,7 @@
},
"discord": {
"description": "Écrire {filepath} pour {client}",
- "description-missing": "Aucun client Discord détecté. Installez vesktop, webcord, armcord, equibop, lightcord ou dorion."
+ "description-missing": "Aucun client Discord détecté. Installez vencord, vesktop, webcord, armcord, equibop, lightcord ou dorion."
},
"pywalfox": {
"description": "Écrire ~/.cache/wal/colors.json et exécuter pywalfox update",
@@ -549,7 +576,7 @@
"description": "Options de configuration supplémentaires.",
"user-templates": {
"label": "Modèles utilisateur",
- "description": "Activer la configuration Matugen définie par l'utilisateur depuis ~/.config/matugen/config.toml"
+ "description": "Activer la configuration Matugen définie par l'utilisateur. Un fichier modèle sera créé dans ~/.config/noctalia/user-templates.toml lors de la première activation"
}
},
"section": {
@@ -576,6 +603,10 @@
"label": "Météo",
"description": "Choisissez votre unité de température préférée."
},
+ "enabled": {
+ "label": "Activer la météo",
+ "description": "Afficher les informations météorologiques dans toute l'interface et récupérer les données météo. Lorsque désactivé, tous les éléments météo seront masqués et aucune requête réseau ne sera effectuée."
+ },
"fahrenheit": {
"label": "Afficher la température en Fahrenheit (°F)",
"description": "Afficher la température en Fahrenheit au lieu de Celsius."
@@ -897,9 +928,13 @@
"label": "Mode de masquage",
"description": "Contrôle le comportement du widget lorsqu'aucune fenêtre n'est active."
},
- "width": {
- "description": "Contrôle la taille horizontale du widget.",
- "label": "Largeur du widget"
+ "max-width": {
+ "label": "Largeur Maximale",
+ "description": "Définit la taille horizontale maximale du widget. Le widget se rétrécira pour s'adapter à un contenu plus court."
+ },
+ "use-fixed-width": {
+ "label": "Utiliser une Largeur Fixe",
+ "description": "Lorsque activé, le widget utilisera toujours la largeur maximale au lieu de s'ajuster dynamiquement au contenu."
},
"colorize-icons": {
"label": "Coloriser les icônes",
@@ -1046,6 +1081,14 @@
"label": "Type de visualiseur",
"description": "Choisir le style de visualiseur audio à afficher."
},
+ "max-width": {
+ "label": "Largeur Maximale",
+ "description": "Définit la taille horizontale maximale du widget. Le widget se rétrécira pour s'adapter à un contenu plus court."
+ },
+ "use-fixed-width": {
+ "label": "Utiliser une Largeur Fixe",
+ "description": "Lorsque activé, le widget utilisera toujours la largeur maximale au lieu de s'ajuster dynamiquement au contenu."
+ },
"scrolling-mode": {
"label": "Mode de défilement",
"description": "Contrôler quand le défilement de texte est activé pour les titres de piste longs."
@@ -1186,6 +1229,9 @@
"calendar": {
"panel": {
"week": "Semaine"
+ },
+ "weather": {
+ "loading": "Chargement de la météo…"
}
},
"tooltips": {
@@ -1305,7 +1351,8 @@
"bottom_left": "En bas à gauche",
"bottom_right": "En bas à droite",
"bottom_center": "En bas au centre",
- "top_center": "En haut au centre"
+ "top_center": "En haut au centre",
+ "center": "Centre"
},
"quickSettingsStyle": {
"modern": "Moderne",
@@ -1615,6 +1662,11 @@
"discharging-rate": "Taux de décharge : {rate} W.",
"charging": "En charge.",
"discharging": "En décharge.",
+ "plugged-in": "Branché.",
+ "idle": "Inactif.",
+ "time-left": "Temps restant : {time}.",
+ "time-until-full": "Temps jusqu'à charge complète : {time}.",
+ "health": "État : {percent}%",
"panel": {
"title": "Seuil de charge",
"full": "Capacité totale ({percent}%)",
@@ -1622,5 +1674,34 @@
"lifespan": "Durée de vie prolongée ({percent}%)",
"disabled": "Gestionnaire de batterie désactivé"
}
+ },
+ "setup": {
+ "customize": {
+ "header": "Personnaliser votre expérience",
+ "subheader": "Ajustez la position de la barre, la densité, l'échelle et plus encore."
+ },
+ "appearance": {
+ "header": "Apparence",
+ "subheader": "Choisissez le mode sombre et la source des couleurs (Matugen ou prédéfinie)."
+ },
+ "wallpaper": {
+ "header": "Choisissez votre fond d'écran",
+ "subheader": "Définissez l'ambiance avec un joli fond.",
+ "select-prompt": "Sélectionnez un fond ci-dessous",
+ "preview-error": "Échec du chargement de l'image",
+ "none-in-dir": "Aucun fond d'écran trouvé dans le répertoire",
+ "no-dir": "Aucun répertoire de fonds d'écran sélectionné",
+ "no-valid": "Aucun fichier image valide trouvé dans : {dir}",
+ "choose-dir": "Choisissez un répertoire contenant vos fonds d'écran",
+ "dir": {
+ "label": "Répertoire des fonds d'écran",
+ "description": "Choisissez le dossier contenant vos fonds d'écran",
+ "browse": "Parcourir le dossier",
+ "select-title": "Sélectionner le dossier des fonds d'écran"
+ }
+ },
+ "welcome": {
+ "note": "Quelques réglages de base pour démarrer — toutes les options sont dans Paramètres"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json
index 118939a8..dd3fe7f0 100644
--- a/Assets/Translations/pt.json
+++ b/Assets/Translations/pt.json
@@ -32,6 +32,16 @@
"reset": "Redefinir raio dos cantos da tela"
}
},
+ "lockscreen": {
+ "section": {
+ "label": "Tela de bloqueio",
+ "description": "Configure o comportamento da tela de bloqueio."
+ },
+ "lock-on-suspend": {
+ "label": "Bloquear ao suspender",
+ "description": "Bloquear automaticamente a tela ao suspender o sistema."
+ }
+ },
"fonts": {
"section": {
"label": "Fontes",
@@ -58,6 +68,17 @@
}
},
"reset-scaling": "Redefinir escala"
+ },
+ "language": {
+ "section": {
+ "label": "Idioma",
+ "description": "Escolha o seu idioma preferido para a aplicação."
+ },
+ "select": {
+ "label": "Idioma da aplicação",
+ "description": "Selecione o idioma usado na interface da aplicação.",
+ "auto-detect": "Automático"
+ }
}
},
"audio": {
@@ -261,6 +282,10 @@
"label": "Distância de flutuação da dock",
"description": "Ajuste a distância de flutuação da borda da tela."
},
+ "icon-size": {
+ "label": "Tamanho do Dock",
+ "description": "Ajustar o tamanho geral do Dock."
+ },
"colorize-icons": {
"label": "Colorir ícones",
"description": "Aplicar cores do tema aos ícones de aplicativos da dock (apenas aplicativos não focados)."
@@ -437,10 +462,6 @@
"label": "Fonte de cor",
"description": "Configurações principais para as cores do Noctalia."
},
- "dark-mode": {
- "label": "Modo escuro",
- "description": "Muda para um tema mais escuro para facilitar a visualização à noite."
- },
"use-wallpaper-colors": {
"label": "Usar cores do papel de parede",
"description": "Gerar esquemas de cores do seu papel de parede usando Matugen. Extrai automaticamente cores para criar um tema coeso."
@@ -450,6 +471,12 @@
"description": "Escolha o algoritmo de geração de esquema de cores para Matugen."
}
},
+ "dark-mode": {
+ "switch": {
+ "label": "Modo escuro",
+ "description": "Muda para um tema mais escuro para facilitar a visualização à noite."
+ }
+ },
"predefined": {
"section": {
"label": "Esquemas de cores predefinidos",
@@ -499,7 +526,7 @@
},
"discord": {
"description": "Escrever {filepath} para {client}",
- "description-missing": "Nenhum cliente Discord detectado. Instale vesktop, webcord, armcord, equibop, lightcord ou dorion."
+ "description-missing": "Nenhum cliente Discord detectado. Instale vencord, vesktop, webcord, armcord, equibop, lightcord ou dorion."
},
"pywalfox": {
"description": "Escrever {filepath} e executar pywalfox update",
@@ -511,7 +538,7 @@
"description": "Opções de configuração adicionais.",
"user-templates": {
"label": "Modelos do usuário",
- "description": "Ativa a configuração do Matugen definida pelo usuário em ~/.config/matugen/config.toml"
+ "description": "Ativa a configuração do Matugen definida pelo usuário. Um arquivo de modelo será criado em ~/.config/noctalia/user-templates.toml na primeira ativação"
}
},
"section": {
@@ -538,6 +565,10 @@
"label": "Clima",
"description": "Escolha sua unidade de temperatura preferida."
},
+ "enabled": {
+ "label": "Ativar clima",
+ "description": "Mostrar informações meteorológicas em toda a interface e buscar dados do clima. Quando desativado, todos os elementos do clima serão ocultados e nenhuma solicitação de rede será feita."
+ },
"fahrenheit": {
"label": "Exibir temperatura em Fahrenheit (°F)",
"description": "Exibe a temperatura em Fahrenheit em vez de Celsius."
@@ -897,9 +928,13 @@
"label": "Modo de rolagem",
"description": "Controlar quando a rolagem de texto está habilitada para títulos de janela longos."
},
- "width": {
- "description": "Controla o tamanho horizontal do widget.",
- "label": "Largura do Widget"
+ "max-width": {
+ "label": "Largura Máxima",
+ "description": "Define o tamanho horizontal máximo do widget. O widget será reduzido para se adequar a conteúdo mais curto."
+ },
+ "use-fixed-width": {
+ "label": "Usar Largura Fixa",
+ "description": "Quando ativado, o widget sempre usará a largura máxima em vez de ajustar dinamicamente ao conteúdo."
},
"colorize-icons": {
"label": "Colorir ícones",
@@ -1050,6 +1085,14 @@
"label": "Tipo de visualizador",
"description": "Escolher o estilo de visualizador de áudio a exibir."
},
+ "max-width": {
+ "label": "Largura Máxima",
+ "description": "Define o tamanho horizontal máximo do widget. O widget será reduzido para se adequar a conteúdo mais curto."
+ },
+ "use-fixed-width": {
+ "label": "Usar Largura Fixa",
+ "description": "Quando ativado, o widget sempre usará a largura máxima em vez de ajustar dinamicamente ao conteúdo."
+ },
"scrolling-mode": {
"label": "Modo de rolagem",
"description": "Controlar quando a rolagem de texto está habilitada para títulos de faixa longos."
@@ -1186,6 +1229,9 @@
"calendar": {
"panel": {
"week": "Semana"
+ },
+ "weather": {
+ "loading": "Carregando o clima…"
}
},
"tooltips": {
@@ -1303,7 +1349,8 @@
"bottom_left": "Inferior esquerdo",
"bottom_right": "Inferior direito",
"bottom_center": "Centro inferior",
- "top_center": "Centro superior"
+ "top_center": "Centro superior",
+ "center": "Centro"
},
"quickSettingsStyle": {
"modern": "Moderno",
@@ -1615,18 +1662,46 @@
"discharging-rate": "Taxa de descarregamento: {rate} W.",
"charging": "Carregando.",
"discharging": "Descarregando.",
- "battery-manager": {
- "title": "Limite da bateria",
- "set-success-desc": "Limite da bateria definido para {percent}%",
- "initial-setup": "Configuração inicial necessária",
- "set-failed": "Falha ao definir o limite da bateria",
- "install-success": "Instalado com sucesso",
- "install-missing": "Arquivos necessários ausentes",
- "install-unsupported": "Sistema não suportado",
- "install-failed": "Falha na instalação",
- "uninstall-setup": "Desinstalando, autenticação necessária",
- "uninstall-success": "Desinstalado com sucesso",
- "uninstall-failed": "Falha na desinstalação"
+ "plugged-in": "Conectado.",
+ "idle": "Ocioso.",
+ "time-left": "Tempo restante: {time}.",
+ "time-until-full": "Tempo até carga completa: {time}.",
+ "health": "Saúde: {percent}%",
+ "panel": {
+ "balanced": "Balanceado ({percent}%)",
+ "disabled": "Gerenciador de bateria desativado",
+ "full": "Capacidade máxima ({percent}%)",
+ "lifespan": "Vida útil prolongada ({percent}%)",
+ "title": "Limite de carga"
+ }
+ },
+ "setup": {
+ "customize": {
+ "header": "Personalizar a sua experiência",
+ "subheader": "Ajuste a posição da barra, densidade, escala e mais."
+ },
+ "appearance": {
+ "header": "Aparência",
+ "subheader": "Escolha o modo escuro e as fontes de cores (Matugen ou predefinidas)."
+ },
+ "wallpaper": {
+ "header": "Escolha o seu papel de parede",
+ "subheader": "Defina o ambiente com um belo fundo.",
+ "select-prompt": "Selecione um papel de parede abaixo",
+ "preview-error": "Falha ao carregar a imagem",
+ "none-in-dir": "Nenhum papel de parede encontrado no diretório",
+ "no-dir": "Nenhum diretório de papéis de parede selecionado",
+ "no-valid": "Nenhuma imagem válida encontrada em: {dir}",
+ "choose-dir": "Escolha um diretório contendo seus papéis de parede",
+ "dir": {
+ "label": "Diretório de papéis de parede",
+ "description": "Escolha a pasta que contém seus papéis de parede",
+ "browse": "Procurar pasta",
+ "select-title": "Selecionar pasta de papéis de parede"
+ }
+ },
+ "welcome": {
+ "note": "Apenas alguns ajustes básicos para começar - o restante está em Configurações"
}
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json
index d64db2ff..4738030b 100644
--- a/Assets/Translations/zh-CN.json
+++ b/Assets/Translations/zh-CN.json
@@ -32,6 +32,16 @@
"reset": "重置屏幕圆角半径"
}
},
+ "lockscreen": {
+ "section": {
+ "label": "锁屏",
+ "description": "配置锁屏行为。"
+ },
+ "lock-on-suspend": {
+ "label": "挂起时锁定",
+ "description": "在系统挂起时自动锁定屏幕。"
+ }
+ },
"fonts": {
"reset-scaling": "恢复默认缩放",
"section": {
@@ -58,6 +68,17 @@
"description": "增大或减小等宽文本的尺寸"
}
}
+ },
+ "language": {
+ "section": {
+ "label": "语言",
+ "description": "选择您偏好的应用程序语言。"
+ },
+ "select": {
+ "label": "应用程序语言",
+ "description": "选择应用程序界面中使用的语言。",
+ "auto-detect": "自动检测"
+ }
}
},
"audio": {
@@ -261,6 +282,10 @@
"label": "Dock 浮动距离",
"description": "调整距离屏幕边缘的浮动距离。"
},
+ "icon-size": {
+ "label": "Dock 大小",
+ "description": "调整 Dock 的整体大小。"
+ },
"colorize-icons": {
"label": "着色图标",
"description": "将主题颜色应用到 Dock 应用图标(仅限非聚焦应用)。"
@@ -475,10 +500,6 @@
"label": "颜色来源",
"description": "Noctalia 颜色的主要设置。"
},
- "dark-mode": {
- "label": "深色模式",
- "description": "切换到更暗的主题,便于夜间观看。"
- },
"use-wallpaper-colors": {
"label": "使用壁纸颜色",
"description": "使用 Matugen 从壁纸生成颜色方案。自动提取颜色以创建一致的主题。"
@@ -488,6 +509,12 @@
"description": "为 Matugen 选择配色方案生成算法。"
}
},
+ "dark-mode": {
+ "switch": {
+ "label": "深色模式",
+ "description": "切换到更暗的主题,便于夜间观看。"
+ }
+ },
"predefined": {
"section": {
"label": "预定义配色方案",
@@ -537,7 +564,7 @@
},
"discord": {
"description": "为 {client} 写入 {filepath}",
- "description-missing": "未检测到 Discord 客户端。请安装 vesktop、webcord、armcord、equibop、lightcord 或 dorion。"
+ "description-missing": "未检测到 Discord 客户端。请安装 vencord、vesktop、webcord、armcord、equibop、lightcord 或 dorion。"
},
"pywalfox": {
"description": "写入 {filepath} 并运行 pywalfox update",
@@ -549,7 +576,7 @@
"description": "其他配置选项。",
"user-templates": {
"label": "用户模板",
- "description": "启用来自 ~/.config/matugen/config.toml 的用户定义 Matugen 配置"
+ "description": "启用用户定义的 Matugen 配置。首次启用时将在 ~/.config/noctalia/user-templates.toml 创建模板文件"
}
},
"section": {
@@ -576,6 +603,10 @@
"label": "天气",
"description": "选择您喜欢的温度单位。"
},
+ "enabled": {
+ "label": "启用天气",
+ "description": "在整个界面中显示天气信息并获取天气数据。禁用后,所有天气元素将被隐藏,不会进行网络请求。"
+ },
"fahrenheit": {
"label": "以华氏度显示温度 (°F)",
"description": "以华氏度而非摄氏度显示温度。"
@@ -897,9 +928,13 @@
"label": "滚动模式",
"description": "控制何时为长窗口标题启用文本滚动。"
},
- "width": {
- "description": "控制小部件的水平尺寸。",
- "label": "小部件宽度"
+ "max-width": {
+ "label": "最大宽度",
+ "description": "设置小部件的最大水平尺寸。当内容较短时,小部件会自动收缩以适应内容。"
+ },
+ "use-fixed-width": {
+ "label": "使用固定宽度",
+ "description": "启用后,小部件将始终使用最大宽度,而不根据内容动态调整。"
},
"colorize-icons": {
"label": "着色图标",
@@ -1050,6 +1085,14 @@
"label": "可视化器类型",
"description": "选择要显示的音频可视化器样式。"
},
+ "max-width": {
+ "label": "最大宽度",
+ "description": "设置小部件的最大水平尺寸。当内容较短时,小部件会自动收缩以适应内容。"
+ },
+ "use-fixed-width": {
+ "label": "使用固定宽度",
+ "description": "启用后,小部件将始终使用最大宽度,而不根据内容动态调整。"
+ },
"scrolling-mode": {
"label": "滚动模式",
"description": "控制何时为长曲目标题启用文本滚动。"
@@ -1186,6 +1229,9 @@
"calendar": {
"panel": {
"week": "周"
+ },
+ "weather": {
+ "loading": "正在加载天气…"
}
},
"tooltips": {
@@ -1305,7 +1351,8 @@
"bottom_left": "左下角",
"bottom_right": "右下角",
"bottom_center": "底部居中",
- "top_center": "顶部居中"
+ "top_center": "顶部居中",
+ "center": "居中"
},
"quickSettingsStyle": {
"modern": "现代",
@@ -1615,6 +1662,11 @@
"discharging-rate": "放电速率:{rate} W。",
"charging": "正在充电。",
"discharging": "正在放电。",
+ "plugged-in": "已接通电源。",
+ "idle": "空闲。",
+ "time-left": "剩余时间:{time}。",
+ "time-until-full": "充满所需时间:{time}。",
+ "health": "健康状况:{percent}%",
"panel": {
"title": "充电阈值",
"full": "完全容量 ({percent}%)",
@@ -1622,5 +1674,34 @@
"lifespan": "延长寿命 ({percent}%)",
"disabled": "电池管理器已禁用"
}
+ },
+ "setup": {
+ "customize": {
+ "header": "自定义体验",
+ "subheader": "调整状态栏位置、密度、缩放等。"
+ },
+ "appearance": {
+ "header": "外观",
+ "subheader": "选择深色模式与配色来源(Matugen 或预设)。"
+ },
+ "wallpaper": {
+ "header": "选择你的壁纸",
+ "subheader": "用精美壁纸营造氛围。",
+ "select-prompt": "在下方选择一张壁纸",
+ "preview-error": "图片加载失败",
+ "none-in-dir": "目录中未找到壁纸",
+ "no-dir": "未选择壁纸目录",
+ "no-valid": "在 {dir} 中未找到有效图片文件",
+ "choose-dir": "选择包含壁纸图片的目录",
+ "dir": {
+ "label": "壁纸目录",
+ "description": "选择存放壁纸的文件夹",
+ "browse": "浏览文件夹",
+ "select-title": "选择壁纸文件夹"
+ }
+ },
+ "welcome": {
+ "note": "先进行一些基础设置——更多选项可在“设置”中找到"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/noctalia.svg b/Assets/noctalia.svg
new file mode 100644
index 00000000..10646225
--- /dev/null
+++ b/Assets/noctalia.svg
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/Assets/settings-default.json b/Assets/settings-default.json
index b8a82259..57033237 100644
--- a/Assets/settings-default.json
+++ b/Assets/settings-default.json
@@ -1,5 +1,6 @@
{
- "settingsVersion": 15,
+ "settingsVersion": 16,
+ "setupCompleted": false,
"bar": {
"position": "top",
"backgroundOpacity": 1,
@@ -64,10 +65,13 @@
"screenRadiusRatio": 1,
"animationSpeed": 1,
"animationDisabled": false,
- "compactLockScreen": false
+ "compactLockScreen": false,
+ "lockOnSuspend": true,
+ "language": ""
},
"location": {
"name": "Tokyo",
+ "weatherEnabled": true,
"useFahrenheit": false,
"use12hourFormat": false,
"showWeekNumberInCalendar": false
@@ -169,7 +173,8 @@
"onlySameOutput": true,
"monitors": [],
"pinnedApps": [],
- "colorizeIcons": false
+ "colorizeIcons": false,
+ "size": 1
},
"network": {
"wifiEnabled": true
@@ -205,7 +210,6 @@
"fontFixed": "DejaVu Sans Mono",
"fontDefaultScale": 1,
"fontFixedScale": 1,
- "idleInhibitorEnabled": false,
"tooltipsEnabled": true
},
"brightness": {
@@ -253,4 +257,4 @@
"battery": {
"chargingMode": 0
}
-}
\ No newline at end of file
+}
diff --git a/Bin/dev/qmlfmt.sh b/Bin/dev/qmlfmt.sh
index a7c6b548..8d09aa3d 100755
--- a/Bin/dev/qmlfmt.sh
+++ b/Bin/dev/qmlfmt.sh
@@ -1,7 +1,34 @@
#!/usr/bin/env -S bash
+set -euo pipefail
+# QML Formatter Script
# Uses: https://github.com/jesperhh/qmlfmt
-# Can be installed from AUR "qmlfmt-git"
-# Requires qt6-5compat
+# Install: AUR package "qmlfmt-git" (requires qt6-5compat)
-find . -name "*.qml" -print -exec qmlfmt -e -b 360 -t 2 -i 2 -w {} \;
+command -v qmlfmt &>/dev/null || { echo "qmlfmt not found" >&2; exit 1; }
+
+format_file() { qmlfmt -e -b 360 -t 2 -i 2 -w "$1" || { echo "Failed: $1" >&2; return 1; }; }
+export -f format_file
+
+# Find all .qml files
+mapfile -t all_files < <(find "${1:-.}" -name "*.qml" -type f)
+[ ${#all_files[@]} -eq 0 ] && { echo "No QML files found"; exit 0; }
+
+echo "Scanning ${#all_files[@]} files for array destructuring..."
+safe_files=()
+for file in "${all_files[@]}"; do
+ # Checks for a comma inside brackets followed by an equals sign aka "array destructuring"
+ # as this ES6 syntax is not supported by qmlfmt and will result in breakage.
+ if grep -qE '\[.*,.*\]\s*=' "$file"; then
+ echo "-> Skipping (Array destructuring detected): $file" >&2
+ else
+ safe_files+=("$file")
+ fi
+done
+
+[ ${#safe_files[@]} -eq 0 ] && { echo "No safe files to format after filtering."; exit 0; }
+
+echo "Formatting ${#safe_files[@]} files..."
+printf '%s\0' "${safe_files[@]}" | \
+ xargs -0 -P "${QMLFMT_JOBS:-$(nproc)}" -I {} bash -c 'format_file "$@"' _ {} \
+ && echo "Done" || { echo "Errors occurred" >&2; exit 1; }
\ No newline at end of file
diff --git a/Commons/Color.qml b/Commons/Color.qml
index 7d1dc68c..4b5884a5 100644
--- a/Commons/Color.qml
+++ b/Commons/Color.qml
@@ -106,11 +106,11 @@ Singleton {
printErrors: false
watchChanges: true
onFileChanged: {
- Logger.log("Color", "Reloading colors from disk")
+ Logger.i("Color", "Reloading colors from disk")
reload()
}
onAdapterUpdated: {
- Logger.log("Color", "Writing colors to disk")
+ Logger.i("Color", "Writing colors to disk")
writeAdapter()
}
diff --git a/Commons/I18n.qml b/Commons/I18n.qml
index 11180047..4ec5e86f 100644
--- a/Commons/I18n.qml
+++ b/Commons/I18n.qml
@@ -8,11 +8,10 @@ import qs.Commons
Singleton {
id: root
- property bool debug: false
- property string debugForceLanguage: ""
property bool isLoaded: false
property string langCode: ""
+ property string systemDetectedLangCode: ""
property var availableLanguages: []
property var translations: ({})
property var fallbackTranslations: ({})
@@ -36,7 +35,7 @@ Singleton {
var output = stdoutCollector.text || ""
parseDirectoryListing(output)
} else {
- Logger.error("I18n", `Failed to scan translation directory`)
+ Logger.e("I18n", `Failed to scan translation directory`)
// Fallback to default languages
availableLanguages = ["en"]
detectLanguage()
@@ -53,21 +52,19 @@ Singleton {
try {
var data = JSON.parse(text())
root.translations = data
- Logger.log("I18n", `Loaded translations for "${root.langCode}"`)
- if (debug) {
- Logger.log("I18n", `Available root keys: ${Object.keys(data).join(", ")}`)
- }
+ Logger.i("I18n", `Loaded translations for "${root.langCode}"`)
+ Logger.d("I18n", `Available root keys: ${Object.keys(data).join(", ")}`)
root.isLoaded = true
root.translationsLoaded()
} catch (e) {
- Logger.error("I18n", `Failed to parse translation file: ${e}`)
+ Logger.e("I18n", `Failed to parse translation file: ${e}`)
setLanguage("en")
}
}
onLoadFailed: function (error) {
setLanguage("en")
- Logger.error("I18n", `Failed to load translation file: ${error}`)
+ Logger.e("I18n", `Failed to load translation file: ${error}`)
}
}
@@ -80,24 +77,24 @@ Singleton {
try {
var data = JSON.parse(text())
root.fallbackTranslations = data
- Logger.log("I18n", `Loaded english fallback translations`)
+ Logger.d("I18n", `Loaded english fallback translations`)
} catch (e) {
- Logger.error("I18n", `Failed to parse fallback translation file: ${e}`)
+ Logger.e("I18n", `Failed to parse fallback translation file: ${e}`)
}
}
onLoadFailed: function (error) {
- Logger.error("I18n", `Failed to load fallback translation file: ${error}`)
+ Logger.e("I18n", `Failed to load fallback translation file: ${error}`)
}
}
Component.onCompleted: {
- Logger.log("I18n", "Service started")
+ Logger.i("I18n", "Service started")
scanAvailableLanguages()
}
// -------------------------------------------
function scanAvailableLanguages() {
- Logger.log("I18n", "Scanning for available translation files...")
+ Logger.d("I18n", "Scanning for available translation files...")
directoryScanner.running = true
}
@@ -107,7 +104,7 @@ Singleton {
try {
if (!output || output.trim() === "") {
- Logger.warn("I18n", "Empty directory listing output")
+ Logger.w("I18n", "Empty directory listing output")
availableLanguages = ["en"]
detectLanguage()
return
@@ -136,17 +133,17 @@ Singleton {
}
if (languages.length === 0) {
- Logger.warn("I18n", "No translation files found, using fallback")
+ Logger.w("I18n", "No translation files found, using fallback")
languages = ["en"] // Fallback
}
availableLanguages = languages
- Logger.log("I18n", `Found ${languages.length} available languages: ${languages.join(', ')}`)
+ Logger.d("I18n", `Found ${languages.length} available languages: ${languages.join(', ')}`)
// Detect language after scanning
detectLanguage()
} catch (e) {
- Logger.error("I18n", `Failed to parse directory listing: ${e}`)
+ Logger.e("I18n", `Failed to parse directory listing: ${e}`)
// Fallback to default languages
availableLanguages = ["en"]
detectLanguage()
@@ -155,59 +152,58 @@ Singleton {
// -------------------------------------------
function detectLanguage() {
- Logger.log("I18n", `detectLanguage() called. Available languages: [${availableLanguages.join(', ')}]`)
+ Logger.d("I18n", `detectLanguage() called. Available languages: [${availableLanguages.join(', ')}]`)
if (availableLanguages.length === 0) {
- Logger.warn("I18n", "No available languages found")
+ Logger.w("I18n", "No available languages found")
return
}
- if (debug && debugForceLanguage !== "") {
- Logger.log("I18n", `Debug mode: forcing language to "${debugForceLanguage}"`)
- if (availableLanguages.includes(debugForceLanguage)) {
- setLanguage(debugForceLanguage)
- return
- } else {
- Logger.warn("I18n", `Debug language "${debugForceLanguage}" not available in [${availableLanguages.join(', ')}]`)
- }
- }
+ var detectedLang = ""
- // Detect user's favorite locale - languages
+ // First, determine the system's preferred language
for (var i = 0; i < Qt.locale().uiLanguages.length; i++) {
const fullUserLang = Qt.locale().uiLanguages[i]
- // Try full code match (such as zh CN, en US)
if (availableLanguages.includes(fullUserLang)) {
- Logger.log("I18n", `Exact match found: "${fullUserLang}"`)
- setLanguage(fullUserLang)
- return
+ detectedLang = fullUserLang
+ break
}
- // If full code match fails, try short code matching (such as zh, en)
const shortUserLang = fullUserLang.substring(0, 2)
if (availableLanguages.includes(shortUserLang)) {
- Logger.log("I18n", `Short code match found: "${shortUserLang}" from "${fullUserLang}"`)
- setLanguage(shortUserLang)
- return
+ detectedLang = shortUserLang
+ break
}
-
- Logger.log("I18n", `No match for system language: "${fullUserLang}"`)
}
- // Fallback to first available language (preferably "en" if available)
- const fallbackLang = availableLanguages.includes("en") ? "en" : availableLanguages[0]
- setLanguage(fallbackLang)
+ // If no system language is found among available languages, fallback
+ if (detectedLang === "") {
+ detectedLang = availableLanguages.includes("en") ? "en" : availableLanguages[0]
+ }
+
+ root.systemDetectedLangCode = detectedLang
+ Logger.d("I18n", `System detected language: "${root.systemDetectedLangCode}"`)
+
+ // Now, apply the language: user-defined, then system-detected
+ if (Settings.data.general.language !== "" && availableLanguages.includes(Settings.data.general.language)) {
+ Logger.d("I18n", `User-defined language found: "${Settings.data.general.language}"`)
+ setLanguage(Settings.data.general.language)
+ } else {
+ Logger.d("I18n", `No user-defined language, using system detected: "${root.systemDetectedLangCode}"`)
+ setLanguage(root.systemDetectedLangCode)
+ }
}
// -------------------------------------------
function setLanguage(newLangCode) {
if (newLangCode !== langCode && availableLanguages.includes(newLangCode)) {
langCode = newLangCode
- Logger.log("I18n", `Language set to "${langCode}"`)
+ Logger.i("I18n", `Language set to "${langCode}"`)
languageChanged(langCode)
loadTranslations()
} else if (!availableLanguages.includes(newLangCode)) {
- Logger.warn("I18n", `Language "${newLangCode}" is not available`)
+ Logger.w("I18n", `Language "${newLangCode}" is not available`)
}
}
@@ -219,7 +215,7 @@ Singleton {
const filePath = `file://${Quickshell.shellDir}/Assets/Translations/${langCode}.json`
fileView.path = filePath
isLoaded = false
- Logger.log("I18n", `Loading translations: ${langCode}`)
+ Logger.d("I18n", `Loading translations: ${langCode}`)
// Only load fallback translations if we are not using english and english is available
if (langCode !== "en" && availableLanguages.includes("en")) {
@@ -271,7 +267,7 @@ Singleton {
// -------------------------------------------
// Reload translations (useful for development)
function reload() {
- Logger.log("I18n", "Reloading translations")
+ Logger.d("I18n", "Reloading translations")
loadTranslations()
}
@@ -282,9 +278,7 @@ Singleton {
interpolations = {}
if (!isLoaded) {
- if (debug) {
- Logger.warn("I18n", "Translations not loaded yet")
- }
+ Logger.d("I18n", "Translations not loaded yet")
return key
}
@@ -294,20 +288,12 @@ Singleton {
// Look-up translation in the active language
var value = translations
var notFound = false
- if (debug) {
- Logger.log("I18n", `Looking up key: "${key}"`)
- }
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === "object" && keys[i] in value) {
value = value[keys[i]]
- if (debug) {
- Logger.log("I18n", `Found key part "${keys[i]}"`)
- }
} else {
- if (debug) {
- Logger.warn("I18n", `Translation key "${key}" not found at part "${keys[i]}"`)
- Logger.warn("I18n", `Available keys: ${Object.keys(value || {}).join(", ")}`)
- }
+ Logger.d("I18n", `Translation key "${key}" not found at part "${keys[i]}"`)
+ Logger.d("I18n", `Available keys: ${Object.keys(value || {}).join(", ")}`)
notFound = true
break
}
@@ -333,9 +319,7 @@ Singleton {
}
if (typeof value !== "string") {
- if (debug) {
- Logger.warn("I18n", `Translation key "${key}" is not a string`)
- }
+ Logger.d("I18n", `Translation key "${key}" is not a string`)
return key
}
diff --git a/Commons/Icons.qml b/Commons/Icons.qml
index 88b9b35b..919782d6 100644
--- a/Commons/Icons.qml
+++ b/Commons/Icons.qml
@@ -26,14 +26,14 @@ Singleton {
signal fontReloaded
Component.onCompleted: {
- Logger.log("Icons", "Service started")
+ Logger.i("Icons", "Service started")
loadFontWithCacheBusting()
}
Connections {
target: Quickshell
function onReloadCompleted() {
- Logger.log("Icons", "Quickshell reload completed - forcing font reload")
+ Logger.d("Icons", "Quickshell reload completed - forcing font reload")
reloadFont()
}
}
@@ -50,7 +50,7 @@ Singleton {
}
function loadFontWithCacheBusting() {
- Logger.log("Icons", "Loading font with cache busting")
+ Logger.d("Icons", "Loading font with cache busting")
// Destroy old loader first
if (currentFontLoader) {
@@ -69,16 +69,16 @@ Singleton {
// Connect to the new loader's status changes
currentFontLoader.statusChanged.connect(function () {
if (currentFontLoader.status === FontLoader.Ready) {
- Logger.log("Icons", "Font loaded successfully:", currentFontLoader.name, "(version " + fontVersion + ")")
+ Logger.d("Icons", "Font loaded successfully:", currentFontLoader.name, "(version " + fontVersion + ")")
fontReloaded()
} else if (currentFontLoader.status === FontLoader.Error) {
- Logger.error("Icons", "Font failed to load (version " + fontVersion + ")")
+ Logger.e("Icons", "Font failed to load (version " + fontVersion + ")")
}
})
}
function reloadFont() {
- Logger.log("Icons", "Forcing font reload...")
+ Logger.d("Icons", "Forcing font reload...")
fontVersion++
loadFontWithCacheBusting()
}
diff --git a/Commons/Logger.qml b/Commons/Logger.qml
index 72b62e46..5613d890 100644
--- a/Commons/Logger.qml
+++ b/Commons/Logger.qml
@@ -25,34 +25,45 @@ Singleton {
}
}
- function log(...args) {
+ // Info log (always visible)
+ function i(...args) {
var msg = _formatMessage(...args)
console.log(msg)
}
- function warn(...args) {
+ // Debug log (only when Settings.isDebug is true)
+ function d(...args) {
+ if (Settings && Settings.isDebug) {
+ var msg = _formatMessage(...args)
+ console.log(msg)
+ }
+ }
+
+ // Warning log
+ function w(...args) {
var msg = _formatMessage(...args)
console.warn(msg)
}
- function error(...args) {
+ // Error log
+ function e(...args) {
var msg = _formatMessage(...args)
console.error(msg)
}
function callStack() {
var stack = _getStackTrace()
- Logger.log("Debug", "--------------------------")
- Logger.log("Debug", "Current call stack")
+ Logger.i("Debug", "--------------------------")
+ Logger.i("Debug", "Current call stack")
// Split the stack into lines and log each one
var stackLines = stack.split('\n')
for (var i = 0; i < stackLines.length; i++) {
var line = stackLines[i].trim() // Remove leading/trailing whitespace
if (line.length > 0) {
// Only log non-empty lines
- Logger.log("Debug", `- ${line}`)
+ Logger.i("Debug", `- ${line}`)
}
}
- Logger.log("Debug", "--------------------------")
+ Logger.i("Debug", "--------------------------")
}
}
diff --git a/Commons/Settings.qml b/Commons/Settings.qml
index d0308487..f98e3e3e 100644
--- a/Commons/Settings.qml
+++ b/Commons/Settings.qml
@@ -14,6 +14,8 @@ Singleton {
readonly property alias data: adapter
property bool isLoaded: false
property bool directoriesCreated: false
+ property int settingsVersion: 16
+ property bool isDebug: Quickshell.env("NOCTALIA_DEBUG") === "1"
// Define our app directories
// Default config directory: ~/.config/noctalia
@@ -92,7 +94,7 @@ Singleton {
}
onLoaded: function () {
if (!isLoaded) {
- Logger.log("Settings", "Settings loaded")
+ Logger.i("Settings", "Settings loaded")
upgradeSettingsData()
validateMonitorConfigurations()
@@ -100,6 +102,9 @@ Singleton {
// Emit the signal
root.settingsLoaded()
+
+ // Finally, update our local settings version
+ adapter.settingsVersion = settingsVersion
}
}
onLoadFailed: function (error) {
@@ -125,7 +130,8 @@ Singleton {
JsonAdapter {
id: adapter
- property int settingsVersion: 15
+ property int settingsVersion: root.settingsVersion
+ property bool setupCompleted: false
// bar
property JsonObject bar: JsonObject {
@@ -185,11 +191,14 @@ Singleton {
property real animationSpeed: 1.0
property bool animationDisabled: false
property bool compactLockScreen: false
+ property bool lockOnSuspend: true
+ property string language: ""
}
// location
property JsonObject location: JsonObject {
property string name: defaultLocation
+ property bool weatherEnabled: true
property bool useFahrenheit: false
property bool use12hourFormat: false
property bool showWeekNumberInCalendar: false
@@ -285,6 +294,7 @@ Singleton {
property string displayMode: "always_visible" // "always_visible", "auto_hide", "exclusive"
property real backgroundOpacity: 1.0
property real floatingRatio: 1.0
+ property real size: 1
property bool onlySameOutput: true
property list monitors: []
// Desktop entry IDs pinned to the dock (e.g., "org.kde.konsole", "firefox.desktop")
@@ -335,7 +345,6 @@ Singleton {
property string fontFixed: "DejaVu Sans Mono"
property real fontDefaultScale: 1.0
property real fontFixedScale: 1.0
- property bool idleInhibitorEnabled: false
property bool tooltipsEnabled: true
}
@@ -348,6 +357,9 @@ Singleton {
property bool useWallpaperColors: false
property string predefinedScheme: "Noctalia (default)"
property bool darkMode: true
+ property string schedulingMode: "off"
+ property string manualSunrise: "06:30"
+ property string manualSunset: "18:30"
property string matugenSchemeType: "scheme-fruit-salad"
property bool generateTemplatesForPredefined: true
}
@@ -428,7 +440,7 @@ Singleton {
// Generate default settings at the root of the repo
function generateDefaultSettings() {
try {
- Logger.log("Settings", "Generating settings-default.json")
+ Logger.d("Settings", "Generating settings-default.json")
// Prepare a clean JSON
var plainAdapter = QtObj2JS.qtObjectToPlainObject(adapter)
@@ -440,7 +452,7 @@ Singleton {
var base64Data = Qt.btoa(jsonData)
Quickshell.execDetached(["sh", "-c", `echo "${base64Data}" | base64 -d > "${defaultPath}"`])
} catch (error) {
- Logger.error("Settings", "Failed to generate default settings file: " + error)
+ Logger.e("Settings", "Failed to generate default settings file: " + error)
}
}
@@ -452,8 +464,8 @@ Singleton {
availableScreenNames.push(Quickshell.screens[i].name)
}
- Logger.log("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]")
- Logger.log("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]")
+ Logger.d("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]")
+ Logger.d("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]")
// Check bar monitors
if (adapter.bar.monitors.length > 0) {
@@ -465,15 +477,15 @@ Singleton {
}
}
if (!hasValidBarMonitor) {
- Logger.warn("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens")
+ Logger.w("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens")
adapter.bar.monitors = []
} else {
- //Logger.log("Settings", "Found valid bar monitors, keeping configuration")
+ //Logger.i("Settings", "Found valid bar monitors, keeping configuration")
}
} else {
- //Logger.log("Settings", "Bar monitor list is empty, will show on all available screens")
+ //Logger.i("Settings", "Bar monitor list is empty, will show on all available screens")
}
}
@@ -483,7 +495,7 @@ Singleton {
function upgradeSettingsData() {
// Wait for BarWidgetRegistry to be ready
if (!BarWidgetRegistry.widgets || Object.keys(BarWidgetRegistry.widgets).length === 0) {
- Logger.warn("Settings", "BarWidgetRegistry not ready, deferring upgrade")
+ Logger.w("Settings", "BarWidgetRegistry not ready, deferring upgrade")
Qt.callLater(upgradeSettingsData)
return
}
@@ -524,7 +536,7 @@ Singleton {
for (var i = widgets.length - 1; i >= 0; i--) {
var widget = widgets[i]
if (!BarWidgetRegistry.hasWidget(widget.id)) {
- Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
+ Logger.w(`Settings`, `Deleted invalid widget ${widget.id}`)
widgets.splice(i, 1)
removedWidget = true
}
@@ -545,7 +557,7 @@ Singleton {
}
if (upgradeWidget(widget)) {
- Logger.log("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
+ Logger.d("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
}
}
}
@@ -571,7 +583,7 @@ Singleton {
adapter.bar.widgets["right"].push(({
"id": "ControlCenter"
}))
- Logger.warn("Settings", "Added a ControlCenter widget to the right section")
+ Logger.w("Settings", "Added a ControlCenter widget to the right section")
}
}
}
diff --git a/Commons/TablerIcons.qml b/Commons/TablerIcons.qml
index 3522e30d..a840d72f 100644
--- a/Commons/TablerIcons.qml
+++ b/Commons/TablerIcons.qml
@@ -128,6 +128,12 @@ Singleton {
"bt-device-watch": "device-watch",
"bt-device-speaker": "device-speaker",
"bt-device-tv": "device-tv",
+ "antenna-bars-1": "antenna-bars-1",
+ "antenna-bars-2": "antenna-bars-2",
+ "antenna-bars-3": "antenna-bars-3",
+ "antenna-bars-4": "antenna-bars-4",
+ "antenna-bars-5": "antenna-bars-5",
+ "antenna-bars-off": "antenna-bars-off",
"noctalia": "noctalia",
"hyprland": "hyprland",
"filepicker-folder": "folder",
diff --git a/Helpers/Debug.js b/Helpers/Debug.js
new file mode 100644
index 00000000..90dc27e7
--- /dev/null
+++ b/Helpers/Debug.js
@@ -0,0 +1,25 @@
+// Helper function to stringify even if there are circular refs.
+function stringify(obj, replacer, spaces, cycleReplacer) {
+ return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces)
+}
+
+function serializer(replacer, cycleReplacer) {
+ var stack = [], keys = []
+
+ if (cycleReplacer == null) cycleReplacer = function(key, value) {
+ if (stack[0] === value) return "[Circular ~]"
+ return "[Circular ~." + keys.slice(0, stack.indexOf(value)).join(".") + "]"
+ }
+
+ return function(key, value) {
+ if (stack.length > 0) {
+ var thisPos = stack.indexOf(this)
+ ~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
+ ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
+ if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value)
+ }
+ else stack.push(value)
+
+ return replacer == null ? value : replacer.call(this, key, value)
+ }
+}
\ No newline at end of file
diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml
index 2bcbb04f..9da4118f 100644
--- a/Modules/Background/Background.qml
+++ b/Modules/Background/Background.qml
@@ -118,7 +118,7 @@ Variants {
sourceSize: undefined
onStatusChanged: {
if (status === Image.Error) {
- Logger.warn("Current wallpaper failed to load:", source)
+ Logger.w("Current wallpaper failed to load:", source)
} else if (status === Image.Ready && !dimensionsCalculated) {
dimensionsCalculated = true
const optimalSize = calculateOptimalWallpaperSize(implicitWidth, implicitHeight)
@@ -147,7 +147,7 @@ Variants {
sourceSize: undefined
onStatusChanged: {
if (status === Image.Error) {
- Logger.warn("Next wallpaper failed to load:", source)
+ Logger.w("Next wallpaper failed to load:", source)
} else if (status === Image.Ready && !dimensionsCalculated) {
dimensionsCalculated = true
const optimalSize = calculateOptimalWallpaperSize(implicitWidth, implicitHeight)
@@ -336,7 +336,7 @@ Variants {
dim = Qt.size(h * imageAspectRatio, h)
}
- Logger.log("Background", `Wallpaper resized on ${modelData.name} ${screenWidth}x${screenHeight} @ ${compositorScale}x`, "src:", wpWidth, wpHeight, "dst:", dim.width, dim.height)
+ Logger.d("Background", `Wallpaper resized on ${modelData.name} ${screenWidth}x${screenHeight} @ ${compositorScale}x`, "src:", wpWidth, wpHeight, "dst:", dim.width, dim.height)
return dim
}
@@ -425,7 +425,7 @@ Variants {
transitionType = "fade"
}
- //Logger.log("Background", "New wallpaper: ", futureWallpaper, "On:", modelData.name, "Transition:", transitionType)
+ //Logger.i("Background", "New wallpaper: ", futureWallpaper, "On:", modelData.name, "Transition:", transitionType)
switch (transitionType) {
case "none":
setWallpaperImmediate(futureWallpaper)
diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml
index 34e673e6..a4009366 100644
--- a/Modules/Background/Overview.qml
+++ b/Modules/Background/Overview.qml
@@ -20,7 +20,7 @@ Variants {
Component.onCompleted: {
if (modelData) {
- Logger.log("Overview", "Loading Overview component for Niri on", modelData.name)
+ Logger.d("Overview", "Loading Overview component for Niri on", modelData.name)
}
setWallpaperInitial()
}
@@ -77,8 +77,8 @@ Variants {
source: bgImage
autoPaddingEnabled: false
blurEnabled: true
- blur: 0.48
- blurMax: 128
+ blur: 1.0
+ blurMax: 64
colorization: Style.opacityMedium
colorizationColor: Settings.data.colorSchemes.darkMode ? Color.mSurface : Color.mOnSurface
}
diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml
index fa61bd95..31504433 100644
--- a/Modules/Bar/Bar.qml
+++ b/Modules/Bar/Bar.qml
@@ -102,6 +102,7 @@ Variants {
model: Settings.data.bar.widgets.left
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
+ barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
@@ -124,6 +125,7 @@ Variants {
model: Settings.data.bar.widgets.center
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
+ barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
@@ -147,6 +149,7 @@ Variants {
model: Settings.data.bar.widgets.right
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
+ barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
@@ -180,6 +183,7 @@ Variants {
model: Settings.data.bar.widgets.left
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
+ barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
@@ -204,6 +208,7 @@ Variants {
model: Settings.data.bar.widgets.center
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
+ barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
@@ -229,6 +234,7 @@ Variants {
model: Settings.data.bar.widgets.right
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
+ barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
diff --git a/Modules/Bar/Bluetooth/BluetoothDevicesList.qml b/Modules/Bar/Bluetooth/BluetoothDevicesList.qml
index 8e144c31..311bfeb4 100644
--- a/Modules/Bar/Bluetooth/BluetoothDevicesList.qml
+++ b/Modules/Bar/Bluetooth/BluetoothDevicesList.qml
@@ -109,7 +109,7 @@ ColumnLayout {
NIcon {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
- text: BluetoothService.getSignalIcon(modelData)
+ icon: BluetoothService.getSignalIcon(modelData)
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurface)
}
diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml
index 68b27ab3..b882324d 100644
--- a/Modules/Bar/Calendar/CalendarPanel.qml
+++ b/Modules/Bar/Calendar/CalendarPanel.qml
@@ -24,7 +24,7 @@ NPanel {
readonly property int firstDayOfWeek: Qt.locale().firstDayOfWeek
property bool isCurrentMonth: checkIsCurrentMonth()
- readonly property bool weatherReady: (LocationService.data.weather !== null)
+ readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null)
function checkIsCurrentMonth() {
return (Time.date.getMonth() === grid.month) && (Time.date.getFullYear() === grid.year)
@@ -78,12 +78,13 @@ NPanel {
// Weather icon and temperature
ColumnLayout {
+ visible: Settings.data.location.weatherEnabled && weatherReady
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginXXS
NIcon {
Layout.alignment: Qt.AlignHCenter
- icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "cloud"
+ icon: Settings.data.location.weatherEnabled && weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : ""
pointSize: Style.fontSizeXXL
color: Color.mOnPrimary
}
@@ -91,6 +92,8 @@ NPanel {
NText {
Layout.alignment: Qt.AlignHCenter
text: {
+ if (!Settings.data.location.weatherEnabled)
+ return ""
if (!weatherReady)
return ""
var temp = LocationService.data.weather.current_weather.temperature
@@ -168,6 +171,8 @@ NPanel {
NText {
text: {
+ if (!Settings.data.location.weatherEnabled)
+ return ""
if (!weatherReady)
return I18n.tr("calendar.weather.loading")
const chunks = Settings.data.location.name.split(",")
@@ -323,7 +328,7 @@ NPanel {
}
}
RowLayout {
- visible: !weatherReady
+ visible: Settings.data.location.weatherEnabled && !weatherReady
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
NBusyIndicator {}
diff --git a/Modules/Bar/Extras/BarPillHorizontal.qml b/Modules/Bar/Extras/BarPillHorizontal.qml
index 0c2b5e90..5a732263 100644
--- a/Modules/Bar/Extras/BarPillHorizontal.qml
+++ b/Modules/Bar/Extras/BarPillHorizontal.qml
@@ -67,7 +67,9 @@ Item {
Connections {
target: root
function onTooltipTextChanged() {
- TooltipService.updateText(root.tooltipText)
+ if (hovered) {
+ TooltipService.updateText(root.tooltipText)
+ }
}
}
diff --git a/Modules/Bar/Extras/BarPillVertical.qml b/Modules/Bar/Extras/BarPillVertical.qml
index c8b53997..485d9732 100644
--- a/Modules/Bar/Extras/BarPillVertical.qml
+++ b/Modules/Bar/Extras/BarPillVertical.qml
@@ -77,7 +77,9 @@ Item {
Connections {
target: root
function onTooltipTextChanged() {
- TooltipService.updateText(root.tooltipText)
+ if (hovered) {
+ TooltipService.updateText(root.tooltipText)
+ }
}
}
diff --git a/Modules/Bar/Extras/BarWidgetLoader.qml b/Modules/Bar/Extras/BarWidgetLoader.qml
index fff11ebf..37ea0391 100644
--- a/Modules/Bar/Extras/BarWidgetLoader.qml
+++ b/Modules/Bar/Extras/BarWidgetLoader.qml
@@ -12,6 +12,9 @@ Item {
property string section: widgetProps && widgetProps.section || ""
property int sectionIndex: widgetProps && widgetProps.sectionWidgetIndex || 0
+ property string barDensity: "default"
+ readonly property real scaling: barDensity === "mini" ? 0.8 : (barDensity === "compact" ? 0.9 : 1.0)
+
// Don't reserve space unless the loaded widget is really visible
implicitWidth: getImplicitSize(loader.item, "implicitWidth")
implicitHeight: getImplicitSize(loader.item, "implicitHeight")
@@ -40,6 +43,12 @@ Item {
item[prop] = widgetProps[prop]
}
}
+ // Explicitly set scaling property
+ if (item.hasOwnProperty("scaling")) {
+ item.scaling = Qt.binding(function () {
+ return root.scaling
+ })
+ }
}
// Register this widget instance with BarService
@@ -51,7 +60,7 @@ Item {
item.onLoaded()
}
- //Logger.log("BarWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
+ //Logger.i("BarWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
}
Component.onDestruction: {
@@ -67,7 +76,7 @@ Item {
// Error handling
onWidgetIdChanged: {
if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) {
- Logger.warn("BarWidgetLoader", "Widget not found in registry:", widgetId)
+ Logger.w("BarWidgetLoader", "Widget not found in registry:", widgetId)
}
}
}
diff --git a/Modules/Bar/Extras/TrayMenu.qml b/Modules/Bar/Extras/TrayMenu.qml
index c0f15e0e..00755184 100644
--- a/Modules/Bar/Extras/TrayMenu.qml
+++ b/Modules/Bar/Extras/TrayMenu.qml
@@ -30,12 +30,12 @@ PopupWindow {
function showAt(item, x, y) {
if (!item) {
- Logger.warn("TrayMenu", "anchorItem is undefined, won't show menu.")
+ Logger.w("TrayMenu", "anchorItem is undefined, won't show menu.")
return
}
if (!opener.children || opener.children.values.length === 0) {
- //Logger.warn("TrayMenu", "Menu not ready, delaying show")
+ //Logger.w("TrayMenu", "Menu not ready, delaying show")
Qt.callLater(() => showAt(item, x, y))
return
}
diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml
index 745f69b5..2058bf1a 100644
--- a/Modules/Bar/Widgets/ActiveWindow.qml
+++ b/Modules/Bar/Widgets/ActiveWindow.qml
@@ -18,6 +18,7 @@ Item {
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
+ property real scaling: 1.0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
@@ -34,7 +35,10 @@ Item {
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
readonly property string hideMode: (widgetSettings.hideMode !== undefined) ? widgetSettings.hideMode : widgetMetadata.hideMode
readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : (widgetMetadata.scrollingMode !== undefined ? widgetMetadata.scrollingMode : "hover")
- readonly property int widgetWidth: (widgetSettings.width !== undefined) ? widgetSettings.width : Math.max(widgetMetadata.width, screen.width * 0.06)
+
+ // Maximum widget width with user settings support
+ readonly property real maxWidth: (widgetSettings.maxWidth !== undefined) ? widgetSettings.maxWidth : Math.max(widgetMetadata.maxWidth, screen ? screen.width * 0.06 : 0)
+ readonly property bool useFixedWidth: (widgetSettings.useFixedWidth !== undefined) ? widgetSettings.useFixedWidth : widgetMetadata.useFixedWidth
readonly property bool isVerticalBar: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right")
readonly property bool hasFocusedWindow: CompositorService.getFocusedWindow() !== null
@@ -42,7 +46,7 @@ Item {
readonly property string fallbackIcon: "user-desktop"
implicitHeight: visible ? (isVerticalBar ? calculatedVerticalDimension() : Style.barHeight) : 0
- implicitWidth: visible ? (isVerticalBar ? calculatedVerticalDimension() : widgetWidth) : 0
+ implicitWidth: visible ? (isVerticalBar ? calculatedVerticalDimension() : dynamicWidth) : 0
// "visible": Always Visible, "hidden": Hide When Empty, "transparent": Transparent When Empty
visible: hideMode !== "hidden" || hasFocusedWindow
@@ -55,8 +59,44 @@ Item {
}
function calculatedVerticalDimension() {
- const ratio = (Settings.data.bar.density === "mini") ? 0.67 : 0.8
- return Math.round(Style.baseWidgetSize * ratio)
+ return Math.round((Style.baseWidgetSize - 5) * scaling)
+ }
+
+ function calculateContentWidth() {
+ // Calculate the actual content width based on visible elements
+ var contentWidth = 0
+ var margins = Style.marginS * scaling * 2 // Left and right margins
+
+ // Icon width (if visible)
+ if (showIcon) {
+ contentWidth += 18 * scaling
+ contentWidth += Style.marginS * scaling // Spacing after icon
+ }
+
+ // Text width (use the measured width)
+ contentWidth += fullTitleMetrics.contentWidth
+
+ // Additional small margin for text
+ contentWidth += Style.marginXXS * 2
+
+ // Add container margins
+ contentWidth += margins
+
+ return Math.ceil(contentWidth)
+ }
+
+ // Dynamic width: adapt to content but respect maximum width setting
+ readonly property real dynamicWidth: {
+ // If using fixed width mode, always use maxWidth
+ if (useFixedWidth) {
+ return maxWidth
+ }
+ // Otherwise, adapt to content
+ if (!hasFocusedWindow) {
+ return maxWidth
+ }
+ // Use content width but don't exceed user-set maximum width
+ return Math.min(calculateContentWidth(), maxWidth)
}
function getAppIcon() {
@@ -72,7 +112,7 @@ Item {
return iconResult
}
} catch (iconError) {
- Logger.warn("ActiveWindow", "Error getting icon from CompositorService:", iconError)
+ Logger.w("ActiveWindow", "Error getting icon from CompositorService:", iconError)
}
}
@@ -90,14 +130,14 @@ Item {
}
}
} catch (fallbackError) {
- Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
+ Logger.w("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
}
}
}
return ThemeIcons.iconFromName(fallbackIcon)
} catch (e) {
- Logger.warn("ActiveWindow", "Error in getAppIcon:", e)
+ Logger.w("ActiveWindow", "Error in getAppIcon:", e)
return ThemeIcons.iconFromName(fallbackIcon)
}
}
@@ -107,7 +147,7 @@ Item {
id: fullTitleMetrics
visible: false
text: windowTitle
- pointSize: Style.fontSizeS
+ pointSize: Style.fontSizeS * scaling
applyUiScale: false
font.weight: Style.fontWeightMedium
}
@@ -117,29 +157,37 @@ Item {
visible: root.visible
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
- width: isVerticalBar ? root.width : widgetWidth
+ width: isVerticalBar ? root.width : dynamicWidth
height: isVerticalBar ? width : Style.capsuleHeight
radius: isVerticalBar ? width / 2 : Style.radiusM
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
+ // Smooth width transition
+ Behavior on width {
+ NumberAnimation {
+ duration: Style.animationNormal
+ easing.type: Easing.InOutCubic
+ }
+ }
+
Item {
id: mainContainer
anchors.fill: parent
- anchors.leftMargin: isVerticalBar ? 0 : Style.marginS
- anchors.rightMargin: isVerticalBar ? 0 : Style.marginS
+ anchors.leftMargin: isVerticalBar ? 0 : Style.marginS * scaling
+ anchors.rightMargin: isVerticalBar ? 0 : Style.marginS * scaling
// Horizontal layout for top/bottom bars
RowLayout {
id: rowLayout
anchors.verticalCenter: parent.verticalCenter
- spacing: Style.marginS
+ spacing: Style.marginS * scaling
visible: !isVerticalBar
z: 1
// Window icon
Item {
- Layout.preferredWidth: 18
- Layout.preferredHeight: 18
+ Layout.preferredWidth: 18 * scaling
+ Layout.preferredHeight: 18 * scaling
Layout.alignment: Qt.AlignVCenter
visible: showIcon
@@ -254,7 +302,7 @@ Item {
NText {
id: titleText
text: windowTitle
- pointSize: Style.fontSizeS
+ pointSize: Style.fontSizeS * scaling
applyUiScale: false
font.weight: Style.fontWeightMedium
verticalAlignment: Text.AlignVCenter
@@ -265,7 +313,7 @@ Item {
NText {
text: windowTitle
font: titleText.font
- pointSize: Style.fontSizeS
+ pointSize: Style.fontSizeS * scaling
applyUiScale: false
verticalAlignment: Text.AlignVCenter
color: Color.mOnSurface
@@ -316,7 +364,7 @@ Item {
// Window icon
Item {
- width: Style.baseWidgetSize * 0.5
+ width: Style.baseWidgetSize * 0.5 * scaling
height: width
anchors.centerIn: parent
visible: windowTitle !== ""
@@ -367,7 +415,7 @@ Item {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) {
- Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e)
+ Logger.w("ActiveWindow", "Error in onActiveWindowChanged:", e)
}
}
function onWindowListChanged() {
@@ -375,7 +423,7 @@ Item {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) {
- Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e)
+ Logger.w("ActiveWindow", "Error in onWindowListChanged:", e)
}
}
}
diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml
index 47676cdb..d29c817e 100644
--- a/Modules/Bar/Widgets/Battery.qml
+++ b/Modules/Bar/Widgets/Battery.qml
@@ -103,28 +103,41 @@ Item {
return lines.join("\n")
}
if (!isReady || !battery.isLaptopBattery) {
- return "No battery detected."
+ return I18n.tr("battery.no-battery-detected")
}
if (battery.timeToEmpty > 0) {
- lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(battery.timeToEmpty)}.`)
+ lines.push(I18n.tr("battery.time-left", {
+ "time": Time.formatVagueHumanReadableDuration(battery.timeToEmpty)
+ }))
}
if (battery.timeToFull > 0) {
- lines.push(`Time until full: ${Time.formatVagueHumanReadableDuration(battery.timeToFull)}.`)
+ lines.push(I18n.tr("battery.time-until-full", {
+ "time": Time.formatVagueHumanReadableDuration(battery.timeToFull)
+ }))
}
if (battery.changeRate !== undefined) {
const rate = battery.changeRate
if (rate > 0) {
- lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W." : "Discharging rate: " + rate.toFixed(2) + " W.")
+ lines.push(charging ? I18n.tr("battery.charging-rate", {
+ "rate": rate.toFixed(2)
+ }) : I18n.tr("battery.discharging-rate", {
+ "rate": rate.toFixed(2)
+ }))
} else if (rate < 0) {
- lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W.")
+ lines.push(I18n.tr("battery.discharging-rate", {
+ "rate": Math.abs(rate).toFixed(2)
+ }))
} else {
- lines.push("Estimating...")
+ // Rate is 0 - check if plugged in (charging state) or idle
+ lines.push(charging ? I18n.tr("battery.plugged-in") : I18n.tr("battery.idle"))
}
} else {
- lines.push(charging ? "Charging." : "Discharging.")
+ lines.push(charging ? I18n.tr("battery.charging") : I18n.tr("battery.discharging"))
}
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
- lines.push("Health: " + Math.round(battery.healthPercentage) + "%")
+ lines.push(I18n.tr("battery.health", {
+ "percent": Math.round(battery.healthPercentage)
+ }))
}
return lines.join("\n")
}
diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml
index cbbecd2f..11cd31cf 100644
--- a/Modules/Bar/Widgets/Clock.qml
+++ b/Modules/Bar/Widgets/Clock.qml
@@ -15,6 +15,7 @@ Rectangle {
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
+ property real scaling: 1.0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
@@ -66,11 +67,13 @@ Rectangle {
visible: text !== ""
text: modelData
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
- pointSize: {
- if (repeater.model.length == 1) {
- return Style.fontSizeS
- } else {
- return (index == 0) ? Style.fontSizeXS : Style.fontSizeXXS
+ Binding on pointSize {
+ value: {
+ if (repeater.model.length == 1) {
+ return Style.fontSizeS * scaling
+ } else {
+ return (index == 0) ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
+ }
}
}
applyUiScale: false
@@ -97,8 +100,11 @@ Rectangle {
visible: text !== ""
text: modelData
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
- pointSize: Style.fontSizeS
+ Binding on pointSize {
+ value: Style.fontSizeS * scaling
+ }
applyUiScale: false
+
font.weight: Style.fontWeightBold
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
wrapMode: Text.WordWrap
diff --git a/Modules/Bar/Widgets/ControlCenter.qml b/Modules/Bar/Widgets/ControlCenter.qml
index b0ceea69..be793115 100644
--- a/Modules/Bar/Widgets/ControlCenter.qml
+++ b/Modules/Bar/Widgets/ControlCenter.qml
@@ -56,7 +56,7 @@ NIconButton {
if (customIconPath !== "")
return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath
if (useDistroLogo)
- return DistroLogoService.osLogo
+ return DistroService.osLogo
return ""
}
visible: source !== ""
diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml
index af0e58cd..4015c4f4 100644
--- a/Modules/Bar/Widgets/CustomButton.qml
+++ b/Modules/Bar/Widgets/CustomButton.qml
@@ -112,7 +112,7 @@ Item {
function onClicked() {
if (leftClickExec) {
Quickshell.execDetached(["sh", "-c", leftClickExec])
- Logger.log("CustomButton", `Executing command: ${leftClickExec}`)
+ Logger.i("CustomButton", `Executing command: ${leftClickExec}`)
} else if (!hasExec) {
// No script was defined, open settings
var settingsPanel = PanelService.getPanel("settingsPanel")
@@ -124,14 +124,14 @@ Item {
function onRightClicked() {
if (rightClickExec) {
Quickshell.execDetached(["sh", "-c", rightClickExec])
- Logger.log("CustomButton", `Executing command: ${rightClickExec}`)
+ Logger.i("CustomButton", `Executing command: ${rightClickExec}`)
}
}
function onMiddleClicked() {
if (middleClickExec) {
Quickshell.execDetached(["sh", "-c", middleClickExec])
- Logger.log("CustomButton", `Executing command: ${middleClickExec}`)
+ Logger.i("CustomButton", `Executing command: ${middleClickExec}`)
}
}
}
diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml
index 922316f4..9831626e 100644
--- a/Modules/Bar/Widgets/MediaMini.qml
+++ b/Modules/Bar/Widgets/MediaMini.qml
@@ -17,6 +17,7 @@ Item {
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
+ property real scaling: 1.0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
@@ -37,8 +38,9 @@ Item {
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : widgetMetadata.scrollingMode
- // Fixed width - no expansion
- readonly property real widgetWidth: Math.max(145, screen.width * 0.06)
+ // Maximum widget width with user settings support
+ readonly property real maxWidth: (widgetSettings.maxWidth !== undefined) ? widgetSettings.maxWidth : Math.max(widgetMetadata.maxWidth, screen ? screen.width * 0.06 : 0)
+ readonly property bool useFixedWidth: (widgetSettings.useFixedWidth !== undefined) ? widgetSettings.useFixedWidth : widgetMetadata.useFixedWidth
readonly property bool hasActivePlayer: MediaService.currentPlayer !== null
readonly property string placeholderText: I18n.tr("bar.widget-settings.media-mini.no-active-player")
@@ -59,7 +61,7 @@ Item {
}
implicitHeight: visible ? (isVerticalBar ? calculatedVerticalDimension() : Style.barHeight) : 0
- implicitWidth: visible ? (isVerticalBar ? calculatedVerticalDimension() : widgetWidth) : 0
+ implicitWidth: visible ? (isVerticalBar ? calculatedVerticalDimension() : dynamicWidth) : 0
// "visible": Always Visible, "hidden": Hide When Empty, "transparent": Transparent When Empty
visible: hideMode !== "hidden" || hasActivePlayer
@@ -76,8 +78,50 @@ Item {
}
function calculatedVerticalDimension() {
- const ratio = (Settings.data.bar.density === "mini") ? 0.67 : 0.8
- return Math.round(Style.baseWidgetSize * ratio)
+ return Math.round((Style.baseWidgetSize - 5) * scaling)
+ }
+
+ function calculateContentWidth() {
+ // Calculate the actual content width based on visible elements
+ var contentWidth = 0
+ var margins = Style.marginS * scaling * 2 // Left and right margins
+
+ // Icon or album art width
+ if (!hasActivePlayer || !showAlbumArt) {
+ // Icon width
+ contentWidth += Style.fontSizeL * scaling
+ } else if (showAlbumArt && hasActivePlayer) {
+ // Album art width
+ contentWidth += 21 * scaling
+ }
+
+ // Spacing between icon/art and text
+ contentWidth += Style.marginS * scaling
+
+ // Text width (use the measured width)
+ contentWidth += fullTitleMetrics.contentWidth
+
+ // Additional small margin for text
+ contentWidth += Style.marginXXS * 2
+
+ // Add container margins
+ contentWidth += margins
+
+ return Math.ceil(contentWidth)
+ }
+
+ // Dynamic width: adapt to content but respect maximum width setting
+ readonly property real dynamicWidth: {
+ // If using fixed width mode, always use maxWidth
+ if (useFixedWidth) {
+ return maxWidth
+ }
+ // Otherwise, adapt to content
+ if (!hasActivePlayer) {
+ return maxWidth
+ }
+ // Use content width but don't exceed user-set maximum width
+ return Math.min(calculateContentWidth(), maxWidth)
}
// A hidden text element to safely measure the full title width
@@ -87,6 +131,7 @@ Item {
text: titleText.text
font: titleText.font
applyUiScale: false
+ pointSize: Style.fontSizeS * scaling
}
Rectangle {
@@ -94,16 +139,24 @@ Item {
visible: root.visible
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
- width: isVerticalBar ? root.width : (widgetWidth)
+ width: isVerticalBar ? root.width : dynamicWidth
height: isVerticalBar ? width : Style.capsuleHeight
radius: isVerticalBar ? width / 2 : Style.radiusM
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
+ // Smooth width transition
+ Behavior on width {
+ NumberAnimation {
+ duration: Style.animationNormal
+ easing.type: Easing.InOutCubic
+ }
+ }
+
Item {
id: mainContainer
anchors.fill: parent
- anchors.leftMargin: isVerticalBar ? 0 : Style.marginS
- anchors.rightMargin: isVerticalBar ? 0 : Style.marginS
+ anchors.leftMargin: isVerticalBar ? 0 : Style.marginS * scaling
+ anchors.rightMargin: isVerticalBar ? 0 : Style.marginS * scaling
Loader {
anchors.verticalCenter: parent.verticalCenter
@@ -155,7 +208,7 @@ Item {
id: rowLayout
anchors.verticalCenter: parent.verticalCenter
- spacing: Style.marginS
+ spacing: Style.marginS * scaling
visible: !isVerticalBar
z: 1 // Above the visualizer
@@ -163,7 +216,7 @@ Item {
id: windowIcon
icon: hasActivePlayer ? (MediaService.isPlaying ? "media-pause" : "media-play") : "disc"
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
- pointSize: Style.fontSizeL
+ pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter
visible: !hasActivePlayer || (!showAlbumArt && !trackArt.visible)
@@ -175,8 +228,8 @@ Item {
spacing: 0
Item {
- Layout.preferredWidth: Math.round(21 * Style.uiScaleRatio)
- Layout.preferredHeight: Math.round(21 * Style.uiScaleRatio)
+ Layout.preferredWidth: Math.round(21 * scaling)
+ Layout.preferredHeight: Math.round(21 * scaling)
NImageCircled {
id: trackArt
@@ -288,7 +341,7 @@ Item {
NText {
id: titleText
text: hasActivePlayer ? getTitle() : placeholderText
- pointSize: Style.fontSizeS
+ pointSize: Style.fontSizeS * scaling
applyUiScale: false
font.weight: Style.fontWeightMedium
verticalAlignment: Text.AlignVCenter
@@ -300,6 +353,7 @@ Item {
text: hasActivePlayer ? getTitle() : placeholderText
font: titleText.font
applyUiScale: false
+ pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
horizontalAlignment: hasActivePlayer ? Text.AlignLeft : Text.AlignHCenter
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
@@ -359,7 +413,7 @@ Item {
anchors.fill: parent
icon: hasActivePlayer ? (MediaService.isPlaying ? "media-pause" : "media-play") : "disc"
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
- pointSize: Style.fontSizeL
+ pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml
index a3058e36..86338210 100644
--- a/Modules/Bar/Widgets/Microphone.qml
+++ b/Modules/Bar/Widgets/Microphone.qml
@@ -51,7 +51,7 @@ Item {
Connections {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onVolumeChanged() {
- // Logger.log("Bar:Microphone", "onInputVolumeChanged")
+ // Logger.i("Bar:Microphone", "onInputVolumeChanged")
if (!firstInputVolumeReceived) {
// Ignore the first volume change
firstInputVolumeReceived = true
@@ -66,7 +66,7 @@ Item {
Connections {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onMutedChanged() {
- // Logger.log("Bar:Microphone", "onInputMutedChanged")
+ // Logger.i("Bar:Microphone", "onInputMutedChanged")
if (!firstInputVolumeReceived) {
// Ignore the first mute change
firstInputVolumeReceived = true
diff --git a/Modules/Bar/Widgets/Taskbar.qml b/Modules/Bar/Widgets/Taskbar.qml
index 3d519dfc..fe08ba01 100644
--- a/Modules/Bar/Widgets/Taskbar.qml
+++ b/Modules/Bar/Widgets/Taskbar.qml
@@ -168,13 +168,13 @@ Rectangle {
try {
CompositorService.focusWindow(taskbarItem.modelData)
} catch (error) {
- Logger.error("Taskbar", "Failed to activate toplevel: " + error)
+ Logger.e("Taskbar", "Failed to activate toplevel: " + error)
}
} else if (mouse.button === Qt.RightButton) {
try {
CompositorService.closeWindow(taskbarItem.modelData)
} catch (error) {
- Logger.error("Taskbar", "Failed to close toplevel: " + error)
+ Logger.e("Taskbar", "Failed to close toplevel: " + error)
}
}
}
diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml
index f76be377..43077266 100644
--- a/Modules/Bar/Widgets/Tray.qml
+++ b/Modules/Bar/Widgets/Tray.qml
@@ -43,7 +43,7 @@ Rectangle {
if (!str || !rule) {
return false
}
- Logger.log("Tray", "wildCardMatch - Input str:", str, "rule:", rule)
+ Logger.i("Tray", "wildCardMatch - Input str:", str, "rule:", rule)
// Escape all special regex characters in the rule
let escapedRule = rule.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
@@ -52,15 +52,15 @@ Rectangle {
// Add ^ and $ to match the entire string
pattern = '^' + pattern + '$'
- Logger.log("Tray", "wildCardMatch - Generated pattern:", pattern)
+ Logger.i("Tray", "wildCardMatch - Generated pattern:", pattern)
try {
const regex = new RegExp(pattern, 'i')
// 'i' for case-insensitive
- Logger.log("Tray", "wildCardMatch - Regex test result:", regex.test(str))
+ Logger.i("Tray", "wildCardMatch - Regex test result:", regex.test(str))
return regex.test(str)
} catch (e) {
- Logger.warn("Tray", "Invalid regex pattern for wildcard match:", rule, e.message)
+ Logger.w("Tray", "Invalid regex pattern for wildcard match:", rule, e.message)
return false // If regex is invalid, it won't match
}
}
@@ -252,7 +252,7 @@ Rectangle {
trayMenu.item.menu = modelData.menu
trayMenu.item.showAt(parent, menuX, menuY)
} else {
- Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
+ Logger.i("Tray", "No menu available for", modelData.id, "or trayMenu not set")
}
}
}
diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml
index 4254b842..10018771 100644
--- a/Modules/Bar/Widgets/Volume.qml
+++ b/Modules/Bar/Widgets/Volume.qml
@@ -51,7 +51,7 @@ Item {
Connections {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
function onVolumeChanged() {
- // Logger.log("Bar:Volume", "onVolumeChanged")
+ // Logger.i("Bar:Volume", "onVolumeChanged")
if (!firstVolumeReceived) {
// Ignore the first volume change
firstVolumeReceived = true
diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml
index a9156e3e..d2b996b5 100644
--- a/Modules/Bar/Widgets/WiFi.qml
+++ b/Modules/Bar/Widgets/WiFi.qml
@@ -37,7 +37,7 @@ NIconButton {
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
} catch (error) {
- Logger.error("Wi-Fi", "Error getting icon:", error)
+ Logger.e("Wi-Fi", "Error getting icon:", error)
return "signal_wifi_bad"
}
}
diff --git a/Modules/ControlCenter/Cards/MediaCard.qml b/Modules/ControlCenter/Cards/MediaCard.qml
index 06891868..d5753455 100644
--- a/Modules/ControlCenter/Cards/MediaCard.qml
+++ b/Modules/ControlCenter/Cards/MediaCard.qml
@@ -11,17 +11,39 @@ import qs.Widgets
NBox {
id: root
- // Background artwork that covers everything
+ // Wrapper - rounded rect clipper
Item {
anchors.fill: parent
- clip: true
+ layer.enabled: true
+ layer.effect: MultiEffect {
+ maskEnabled: true
+ maskThresholdMin: 0.5
+ maskSpreadAtMin: 0.0
+ maskSource: ShaderEffectSource {
+ sourceItem: Rectangle {
+ width: root.width
+ height: root.height
+ radius: Style.radiusM
+ color: "white"
+ }
+ }
+ }
- NImageRounded {
- id: bgArtImage
+ // Background image that covers everything
+ Image {
+ readonly property int dim: Math.round(256 * Style.uiScaleRatio)
+ id: bgImage
anchors.fill: parent
- imagePath: MediaService.trackArtUrl
- imageRadius: Style.radiusM
- visible: MediaService.trackArtUrl !== ""
+ source: MediaService.trackArtUrl || WallpaperService.getWallpaper(Screen.name)
+ sourceSize: Qt.size(dim, dim)
+ fillMode: Image.PreserveAspectCrop
+
+ layer.enabled: true
+ layer.effect: MultiEffect {
+ blurEnabled: true
+ blur: 0.25
+ blurMax: 16
+ }
}
// Dark overlay for readability
@@ -40,27 +62,9 @@ NBox {
border.width: 1
radius: Style.radiusM
}
- }
-
- // Background visualizer on top of the artwork
- Item {
- id: visualizerContainer
- anchors.fill: parent
- layer.enabled: true
- layer.effect: MultiEffect {
- maskEnabled: true
- maskThresholdMin: 0.5
- maskSpreadAtMin: 0.0
- maskSource: ShaderEffectSource {
- sourceItem: Rectangle {
- width: root.width
- height: root.height
- radius: Style.radiusM
- color: "white"
- }
- }
- }
+ //}
+ // Background visualizer on top of the artwork
Loader {
anchors.fill: parent
active: Settings.data.audio.visualizerType !== "" && Settings.data.audio.visualizerType !== "none"
@@ -84,7 +88,7 @@ NBox {
anchors.fill: parent
values: CavaService.values
fillColor: Color.mPrimary
- opacity: MediaService.trackArtUrl !== "" ? 0.5 : 0.8
+ opacity: 0.5
}
}
@@ -94,7 +98,7 @@ NBox {
anchors.fill: parent
values: CavaService.values
fillColor: Color.mPrimary
- opacity: MediaService.trackArtUrl !== "" ? 0.5 : 0.8
+ opacity: 0.5
}
}
@@ -104,7 +108,7 @@ NBox {
anchors.fill: parent
values: CavaService.values
fillColor: Color.mPrimary
- opacity: MediaService.trackArtUrl !== "" ? 0.5 : 0.8
+ opacity: 0.5
}
}
}
diff --git a/Modules/ControlCenter/Cards/WeatherCard.qml b/Modules/ControlCenter/Cards/WeatherCard.qml
index e6b366ff..4b5fe5df 100644
--- a/Modules/ControlCenter/Cards/WeatherCard.qml
+++ b/Modules/ControlCenter/Cards/WeatherCard.qml
@@ -9,7 +9,7 @@ import qs.Widgets
NBox {
id: root
- readonly property bool weatherReady: (LocationService.data.weather !== null)
+ readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null)
ColumnLayout {
id: content
diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml
index 599dddb6..fcc13cb8 100644
--- a/Modules/ControlCenter/ControlCenterPanel.qml
+++ b/Modules/ControlCenter/ControlCenterPanel.qml
@@ -17,9 +17,13 @@ NPanel {
var count = 0
for (var i = 0; i < Settings.data.controlCenter.cards.length; i++) {
const card = Settings.data.controlCenter.cards[i]
- if (!card.enabled) {
+ if (!card.enabled)
continue
- }
+
+ const contributes = (card.id !== "weather-card" || Settings.data.location.weatherEnabled)
+ if (!contributes)
+ continue
+
count++
switch (card.id) {
case "profile-card":
@@ -46,8 +50,8 @@ NPanel {
// Positioning
readonly property string controlCenterPosition: Settings.data.controlCenter.position
- panelAnchorHorizontalCenter: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_center")
- panelAnchorVerticalCenter: false
+ panelAnchorHorizontalCenter: controlCenterPosition !== "close_to_bar_button" && (controlCenterPosition.endsWith("_center") || controlCenterPosition === "center")
+ panelAnchorVerticalCenter: controlCenterPosition === "center"
panelAnchorLeft: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_left")
panelAnchorRight: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_right")
panelAnchorBottom: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("bottom_")
@@ -72,7 +76,7 @@ NPanel {
Repeater {
model: Settings.data.controlCenter.cards
Loader {
- active: modelData.enabled
+ active: modelData.enabled && (modelData.id !== "weather-card" || Settings.data.location.weatherEnabled)
visible: active
Layout.fillWidth: true
Layout.preferredHeight: {
diff --git a/Modules/ControlCenter/ControlCenterWidgetLoader.qml b/Modules/ControlCenter/ControlCenterWidgetLoader.qml
index 5dbe1338..e0360d4c 100644
--- a/Modules/ControlCenter/ControlCenterWidgetLoader.qml
+++ b/Modules/ControlCenter/ControlCenterWidgetLoader.qml
@@ -46,7 +46,7 @@ Item {
item.onLoaded()
}
- //Logger.log("ControlCenterWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
+ //Logger.i("ControlCenterWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
}
Component.onDestruction: {
@@ -58,7 +58,7 @@ Item {
// Error handling
onWidgetIdChanged: {
if (widgetId && !ControlCenterWidgetRegistry.hasWidget(widgetId)) {
- Logger.warn("ControlCenterWidgetLoader", "Widget not found in registry:", widgetId)
+ Logger.w("ControlCenterWidgetLoader", "Widget not found in registry:", widgetId)
}
}
}
diff --git a/Modules/ControlCenter/Widgets/WiFi.qml b/Modules/ControlCenter/Widgets/WiFi.qml
index faaedc73..eea64700 100644
--- a/Modules/ControlCenter/Widgets/WiFi.qml
+++ b/Modules/ControlCenter/Widgets/WiFi.qml
@@ -23,7 +23,7 @@ NIconButtonHot {
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
} catch (error) {
- Logger.error("Wi-Fi", "Error getting icon:", error)
+ Logger.e("Wi-Fi", "Error getting icon:", error)
return "signal_wifi_bad"
}
}
diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml
index 56a932e5..8e60bbf3 100644
--- a/Modules/Dock/Dock.qml
+++ b/Modules/Dock/Dock.qml
@@ -63,7 +63,7 @@ Variants {
readonly property int hideAnimationDuration: Style.animationFast
readonly property int showAnimationDuration: Style.animationFast
readonly property int peekHeight: 1
- readonly property int iconSize: 36
+ readonly property int iconSize: Math.round(12 + 24 * (Settings.data.dock.size ?? 1))
readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL
// Bar detection and positioning properties
@@ -199,15 +199,9 @@ Variants {
color: Color.transparent
WlrLayershell.namespace: "noctalia-dock-peek"
- WlrLayershell.exclusionMode: ExclusionMode.Auto // Always exclusive
-
+ WlrLayershell.exclusionMode: ExclusionMode.Ignore
implicitHeight: peekHeight
- Rectangle {
- anchors.fill: parent
- color: barAtBottom ? Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) : Color.transparent
- }
-
MouseArea {
id: peekArea
anchors.fill: parent
@@ -261,12 +255,6 @@ Variants {
}
}
- // Rectangle {
- // anchors.fill: parent
- // color: "#000FF0"
- // z: -1
- // }
-
// Wrapper item for scale/opacity animations
Item {
id: dockContainerWrapper
diff --git a/Modules/Dock/DockMenu.qml b/Modules/Dock/DockMenu.qml
index d09ea6e7..63ad8017 100644
--- a/Modules/Dock/DockMenu.qml
+++ b/Modules/Dock/DockMenu.qml
@@ -121,7 +121,7 @@ PopupWindow {
function show(item, toplevelData) {
if (!item) {
- Logger.warn("DockMenu", "anchorItem is undefined, won't show menu.")
+ Logger.w("DockMenu", "anchorItem is undefined, won't show menu.")
return
}
@@ -176,7 +176,7 @@ PopupWindow {
Qt.callLater(root.onAppClosed)
}
} else {
- Logger.warn("DockMenu", "Cannot close app - invalid toplevel reference")
+ Logger.w("DockMenu", "Cannot close app - invalid toplevel reference")
}
root.hide()
root.requestClose()
diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml
index 271d4c4b..f8ee5db1 100644
--- a/Modules/Launcher/Launcher.qml
+++ b/Modules/Launcher/Launcher.qml
@@ -127,27 +127,27 @@ NPanel {
const appsPlugin = Qt.createComponent("Plugins/ApplicationsPlugin.qml").createObject(this)
if (appsPlugin) {
registerPlugin(appsPlugin)
- Logger.log("Launcher", "Registered: ApplicationsPlugin")
+ Logger.d("Launcher", "Registered: ApplicationsPlugin")
} else {
- Logger.error("Launcher", "Failed to load ApplicationsPlugin")
+ Logger.e("Launcher", "Failed to load ApplicationsPlugin")
}
// Load calculator plugin
const calcPlugin = Qt.createComponent("Plugins/CalculatorPlugin.qml").createObject(this)
if (calcPlugin) {
registerPlugin(calcPlugin)
- Logger.log("Launcher", "Registered: CalculatorPlugin")
+ Logger.d("Launcher", "Registered: CalculatorPlugin")
} else {
- Logger.error("Launcher", "Failed to load CalculatorPlugin")
+ Logger.e("Launcher", "Failed to load CalculatorPlugin")
}
// Load clipboard history plugin
const clipboardPlugin = Qt.createComponent("Plugins/ClipboardPlugin.qml").createObject(this)
if (clipboardPlugin) {
registerPlugin(clipboardPlugin)
- Logger.log("Launcher", "Registered: ClipboardPlugin")
+ Logger.d("Launcher", "Registered: ClipboardPlugin")
} else {
- Logger.error("Launcher", "Failed to load ClipboardPlugin")
+ Logger.e("Launcher", "Failed to load ClipboardPlugin")
}
}
@@ -207,17 +207,15 @@ NPanel {
// ---------------------
// Navigation
- function selectNext() {
+ function selectNextWrapped() {
if (results.length > 0) {
- // Clamp the index to not exceed the last item
- selectedIndex = Math.min(selectedIndex + 1, results.length - 1)
+ selectedIndex = (selectedIndex + 1) % results.length
}
}
- function selectPrevious() {
+ function selectPreviousWrapped() {
if (results.length > 0) {
- // Clamp the index to not go below the first item (0)
- selectedIndex = Math.max(selectedIndex - 1, 0)
+ selectedIndex = (((selectedIndex - 1) % results.length) + results.length) % results.length
}
}
@@ -257,13 +255,25 @@ NPanel {
Shortcut {
sequence: "Ctrl+K"
- onActivated: ui.selectPrevious()
+ onActivated: ui.selectPreviousWrapped()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "Ctrl+J"
- onActivated: ui.selectNext()
+ onActivated: ui.selectNextWrapped()
+ enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
+ }
+
+ Shortcut {
+ sequence: "Tab"
+ onActivated: ui.selectNextWrapped()
+ enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
+ }
+
+ Shortcut {
+ sequence: "Shift+Tab"
+ onActivated: ui.selectPreviousWrapped()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
@@ -327,10 +337,10 @@ NPanel {
}
})
searchInput.inputItem.Keys.onDownPressed.connect(function (event) {
- ui.selectNext()
+ ui.selectNextWrapped()
})
searchInput.inputItem.Keys.onUpPressed.connect(function (event) {
- ui.selectPrevious()
+ ui.selectPreviousWrapped()
})
searchInput.inputItem.Keys.onReturnPressed.connect(function (event) {
ui.activate()
diff --git a/Modules/Launcher/Plugins/ApplicationsPlugin.qml b/Modules/Launcher/Plugins/ApplicationsPlugin.qml
index 07a8b804..66ad3880 100644
--- a/Modules/Launcher/Plugins/ApplicationsPlugin.qml
+++ b/Modules/Launcher/Plugins/ApplicationsPlugin.qml
@@ -54,7 +54,7 @@ Item {
function loadApplications() {
if (typeof DesktopEntries === 'undefined') {
- Logger.warn("ApplicationsPlugin", "DesktopEntries service not available")
+ Logger.w("ApplicationsPlugin", "DesktopEntries service not available")
return
}
@@ -64,7 +64,7 @@ Item {
app.executableName = getExecutableName(app)
return app
})
- Logger.log("ApplicationsPlugin", `Loaded ${entries.length} applications`)
+ Logger.d("ApplicationsPlugin", `Loaded ${entries.length} applications`)
}
function getExecutableName(app) {
@@ -198,12 +198,12 @@ Item {
// Ensures we are not preventing the future focusing of the app
launcher.close()
- Logger.log("ApplicationsPlugin", `Launching: ${app.name}`)
+ Logger.d("ApplicationsPlugin", `Launching: ${app.name}`)
// Record usage and persist asynchronously
if (Settings.data.appLauncher.sortByMostUsed)
recordUsage(app)
if (Settings.data.appLauncher.useApp2Unit && app.id) {
- Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`)
+ Logger.d("ApplicationsPlugin", `Using app2unit for: ${app.id}`)
if (app.runInTerminal)
Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"])
else
@@ -212,7 +212,7 @@ Item {
// Fallback logic when app2unit is not used
if (app.runInTerminal) {
// If app.execute() fails for terminal apps, we handle it manually.
- Logger.log("ApplicationsPlugin", "Executing terminal app manually: " + app.name)
+ Logger.d("ApplicationsPlugin", "Executing terminal app manually: " + app.name)
const terminal = Settings.data.appLauncher.terminalCommand.split(" ")
const command = terminal.concat(app.command)
Quickshell.execDetached(command)
@@ -220,7 +220,7 @@ Item {
// Default execution for GUI apps
app.execute()
} else {
- Logger.log("ApplicationsPlugin", `Could not launch: ${app.name}. No valid launch method.`)
+ Logger.w("ApplicationsPlugin", `Could not launch: ${app.name}. No valid launch method.`)
}
}
}
diff --git a/Modules/Launcher/Plugins/ClipboardPlugin.qml b/Modules/Launcher/Plugins/ClipboardPlugin.qml
index 1969606c..0d5837bd 100644
--- a/Modules/Launcher/Plugins/ClipboardPlugin.qml
+++ b/Modules/Launcher/Plugins/ClipboardPlugin.qml
@@ -40,7 +40,7 @@ Item {
// Initialize plugin
function init() {
- Logger.log("ClipboardPlugin", "Initialized")
+ Logger.i("ClipboardPlugin", "Initialized")
// Pre-load clipboard data if service is active
if (ClipboardService.active) {
ClipboardService.list(100)
@@ -189,7 +189,7 @@ Item {
})
}
- //Logger.log("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`)
+ //Logger.i("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`)
return results
}
diff --git a/Modules/LockScreen/LockContext.qml b/Modules/LockScreen/LockContext.qml
index b7661919..ed31d0fa 100644
--- a/Modules/LockScreen/LockContext.qml
+++ b/Modules/LockScreen/LockContext.qml
@@ -33,7 +33,7 @@ Scope {
errorMessage = ""
showFailure = false
- Logger.log("LockContext", "Starting PAM authentication for user:", pam.user)
+ Logger.i("LockContext", "Starting PAM authentication for user:", pam.user)
pam.start()
}
@@ -43,7 +43,7 @@ Scope {
user: Quickshell.env("USER")
onPamMessage: {
- Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired)
+ Logger.i("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired)
if (messageIsError) {
errorMessage = message
@@ -52,26 +52,26 @@ Scope {
}
if (responseRequired) {
- Logger.log("LockContext", "Responding to PAM with password")
+ Logger.i("LockContext", "Responding to PAM with password")
respond(root.currentText)
}
}
onResponseRequiredChanged: {
- Logger.log("LockContext", "Response required changed:", responseRequired)
+ Logger.i("LockContext", "Response required changed:", responseRequired)
if (responseRequired && root.unlockInProgress) {
- Logger.log("LockContext", "Automatically responding to PAM")
+ Logger.i("LockContext", "Automatically responding to PAM")
respond(root.currentText)
}
}
onCompleted: result => {
- Logger.log("LockContext", "PAM completed with result:", result)
+ Logger.i("LockContext", "PAM completed with result:", result)
if (result === PamResult.Success) {
- Logger.log("LockContext", "Authentication successful")
+ Logger.i("LockContext", "Authentication successful")
root.unlocked()
} else {
- Logger.log("LockContext", "Authentication failed")
+ Logger.i("LockContext", "Authentication failed")
errorMessage = "Authentication failed"
showFailure = true
root.failed()
@@ -80,7 +80,7 @@ Scope {
}
onError: {
- Logger.log("LockContext", "PAM error:", error, "message:", message)
+ Logger.i("LockContext", "PAM error:", error, "message:", message)
errorMessage = message || "Authentication error"
showFailure = true
root.unlockInProgress = false
diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml
index 8b646e57..09a91ff8 100644
--- a/Modules/LockScreen/LockScreen.qml
+++ b/Modules/LockScreen/LockScreen.qml
@@ -548,16 +548,18 @@ Loader {
anchors.margins: 14
spacing: 14
- // Weather section
+ // Top info row
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 65
spacing: 18
- visible: !Settings.data.general.compactLockScreen && LocationService.coordinatesReady && LocationService.data.weather !== null
+ visible: !Settings.data.general.compactLockScreen
// Media widget with visualizer
Rectangle {
Layout.preferredWidth: 220
+ // Expand to take remaining space when weather is hidden
+ Layout.fillWidth: !(Settings.data.location.weatherEnabled && LocationService.data.weather !== null)
Layout.preferredHeight: 50
radius: 25
color: Color.transparent
@@ -666,6 +668,7 @@ Loader {
// Current weather
RowLayout {
+ visible: Settings.data.location.weatherEnabled && LocationService.data.weather !== null
Layout.preferredWidth: 180
spacing: 8
@@ -738,6 +741,7 @@ Loader {
// 3-day forecast
RowLayout {
+ visible: Settings.data.location.weatherEnabled && LocationService.data.weather !== null
Layout.preferredWidth: 260
Layout.rightMargin: 8
spacing: 4
@@ -785,9 +789,16 @@ 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)
ColumnLayout {
Layout.preferredWidth: 60
+ Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
spacing: 8
// Battery
@@ -851,6 +862,8 @@ Loader {
border.color: passwordInput.activeFocus ? Color.mPrimary : Qt.alpha(Color.mOutline, 0.3)
border.width: passwordInput.activeFocus ? 2 : 1
+ property bool passwordVisible: false
+
Row {
anchors.left: parent.left
anchors.leftMargin: 18
@@ -873,7 +886,7 @@ Loader {
enabled: !lockContext.unlockInProgress
font.pointSize: Style.fontSizeM
color: Color.mPrimary
- echoMode: TextInput.Password
+ echoMode: parent.parent.passwordVisible ? TextInput.Normal : TextInput.Password
passwordCharacter: "•"
passwordMaskDelay: 0
text: lockContext.currentText
@@ -912,23 +925,40 @@ Loader {
}
}
- Row {
- spacing: 6
- visible: passwordInput.text.length > 0
+ // Password display - show dots or actual text based on passwordVisible
+ Item {
+ width: passwordDisplayContent.width
+ height: 20
+ visible: passwordInput.text.length > 0 && !parent.parent.parent.passwordVisible
anchors.verticalCenter: parent.verticalCenter
- Repeater {
- model: passwordInput.text.length
+ Row {
+ id: passwordDisplayContent
+ spacing: 6
+ anchors.verticalCenter: parent.verticalCenter
- NIcon {
- icon: "circle-filled"
- pointSize: Style.fontSizeS
- color: Color.mPrimary
- opacity: 1.0
+ Repeater {
+ model: passwordInput.text.length
+
+ NIcon {
+ icon: "circle-filled"
+ pointSize: Style.fontSizeS
+ color: Color.mPrimary
+ opacity: 1.0
+ }
}
}
}
+ NText {
+ text: passwordInput.text
+ color: Color.mPrimary
+ pointSize: Style.fontSizeM
+ font.weight: Font.Medium
+ visible: passwordInput.text.length > 0 && parent.parent.parent.passwordVisible
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
Rectangle {
width: 2
height: 20
@@ -952,7 +982,43 @@ Loader {
}
}
+ // Eye button to toggle password visibility
Rectangle {
+ anchors.right: submitButton.left
+ anchors.rightMargin: 4
+ anchors.verticalCenter: parent.verticalCenter
+ width: 36
+ height: 36
+ radius: width * 0.5
+ color: eyeButtonArea.containsMouse ? Qt.alpha(Color.mOnSurface, 0.1) : "transparent"
+ visible: passwordInput.text.length > 0
+ enabled: !lockContext.unlockInProgress
+
+ NIcon {
+ anchors.centerIn: parent
+ icon: parent.parent.passwordVisible ? "eye-off" : "eye"
+ pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ }
+
+ MouseArea {
+ id: eyeButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: parent.parent.passwordVisible = !parent.parent.passwordVisible
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: 200
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
+ // Submit button
+ Rectangle {
+ id: submitButton
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml
index 3c97971a..00b93297 100644
--- a/Modules/Notification/Notification.qml
+++ b/Modules/Notification/Notification.qml
@@ -248,10 +248,35 @@ Variants {
// Staggered animation delay based on index
readonly property int animationDelay: index * 100
+ property int hoverCount: 0
+
+ onHoverCountChanged: {
+ if (hoverCount > 0) {
+ resumeTimer.stop()
+ NotificationService.pauseTimeout(notificationId)
+ } else {
+ resumeTimer.start()
+ }
+ }
+
+ Timer {
+ id: resumeTimer
+ interval: 50
+ repeat: false
+ onTriggered: {
+ if (hoverCount === 0) {
+ NotificationService.resumeTimeout(notificationId)
+ }
+ }
+ }
+
// Right-click to dismiss
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
+ hoverEnabled: true
+ onEntered: parent.hoverCount++
+ onExited: parent.hoverCount--
onClicked: {
if (mouse.button === Qt.RightButton) {
animateOut()
@@ -484,6 +509,9 @@ Variants {
delegate: NButton {
property var actionData: modelData
+ onEntered: card.hoverCount++
+ onExited: card.hoverCount--
+
text: {
var actionText = actionData.text || "Open"
// If text contains comma, take the part after the comma (the display text)
diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml
index 23c63a0b..5c98c45d 100644
--- a/Modules/OSD/OSD.qml
+++ b/Modules/OSD/OSD.qml
@@ -35,8 +35,7 @@ Variants {
// Input volume properties
readonly property real currentInputVolume: AudioService.inputVolume
readonly property bool isInputMuted: AudioService.inputMuted
- property bool inputVolumeInitialized: false
- property bool inputMuteInitialized: false
+ property bool inputAudioInitialized: false
// Brightness properties
property real lastUpdatedBrightness: 0
@@ -480,15 +479,17 @@ Variants {
}
function onInputVolumeChanged() {
- if (inputVolumeInitialized) {
- showOSD("inputVolume")
+ if (!inputAudioInitialized) {
+ return
}
+ showOSD("inputVolume")
}
function onInputMutedChanged() {
- if (inputMuteInitialized) {
- showOSD("inputVolume")
+ if (!inputAudioInitialized) {
+ return
}
+ showOSD("inputVolume")
}
}
@@ -500,9 +501,8 @@ Variants {
onTriggered: {
volumeInitialized = true
muteInitialized = true
- inputVolumeInitialized = true
- inputMuteInitialized = true
- // Don't initialize brightness here - let it initialize on first change like volume
+ inputAudioInitialized = true
+ // Brightness initializes on first change to avoid showing OSD on startup
connectBrightnessMonitors()
}
}
diff --git a/Modules/SessionMenu/SessionMenu.qml b/Modules/SessionMenu/SessionMenu.qml
index 7bdbf22c..b4a2bc15 100644
--- a/Modules/SessionMenu/SessionMenu.qml
+++ b/Modules/SessionMenu/SessionMenu.qml
@@ -13,8 +13,8 @@ import qs.Widgets
NPanel {
id: root
- preferredWidth: 320 * Style.uiScaleRatio
- preferredHeight: 360 * Style.uiScaleRatio
+ preferredWidth: 400 * Style.uiScaleRatio
+ preferredHeight: 340 * Style.uiScaleRatio
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
panelKeyboardFocus: true
@@ -31,10 +31,6 @@ NPanel {
"action": "lock",
"icon": "lock",
"title": I18n.tr("session-menu.lock")
- }, {
- "action": "lockAndSuspend",
- "icon": "lock-pause",
- "title": I18n.tr("session-menu.lock-and-suspend")
}, {
"action": "suspend",
"icon": "suspend",
@@ -96,11 +92,13 @@ NPanel {
lockScreen.active = true
}
break
- case "lockAndSuspend":
- CompositorService.lockAndSuspend()
- break
case "suspend":
- CompositorService.suspend()
+ // Check if we should lock before suspending
+ if (Settings.data.general.lockOnSuspend) {
+ CompositorService.lockAndSuspend()
+ } else {
+ CompositorService.suspend()
+ }
break
case "reboot":
CompositorService.reboot()
@@ -119,15 +117,15 @@ NPanel {
}
// Navigation functions
- function selectNext() {
+ function selectNextWrapped() {
if (powerOptions.length > 0) {
- selectedIndex = Math.min(selectedIndex + 1, powerOptions.length - 1)
+ selectedIndex = (selectedIndex + 1) % powerOptions.length
}
}
- function selectPrevious() {
+ function selectPreviousWrapped() {
if (powerOptions.length > 0) {
- selectedIndex = Math.max(selectedIndex - 1, 0)
+ selectedIndex = (((selectedIndex - 1) % powerOptions.length) + powerOptions.length) % powerOptions.length
}
}
@@ -170,25 +168,37 @@ NPanel {
// Keyboard shortcuts
Shortcut {
sequence: "Ctrl+K"
- onActivated: ui.selectPrevious()
+ onActivated: ui.selectPreviousWrapped()
enabled: root.opened
}
Shortcut {
sequence: "Ctrl+J"
- onActivated: ui.selectNext()
+ onActivated: ui.selectNextWrapped()
enabled: root.opened
}
Shortcut {
sequence: "Up"
- onActivated: ui.selectPrevious()
+ onActivated: ui.selectPreviousWrapped()
enabled: root.opened
}
Shortcut {
sequence: "Down"
- onActivated: ui.selectNext()
+ onActivated: ui.selectNextWrapped()
+ enabled: root.opened
+ }
+
+ Shortcut {
+ sequence: "Shift+Tab"
+ onActivated: ui.selectPreviousWrapped()
+ enabled: root.opened
+ }
+
+ Shortcut {
+ sequence: "Tab"
+ onActivated: ui.selectNextWrapped()
enabled: root.opened
}
@@ -231,14 +241,6 @@ NPanel {
}
// Navigation functions
- function selectNext() {
- root.selectNext()
- }
-
- function selectPrevious() {
- root.selectPrevious()
- }
-
function selectFirst() {
root.selectFirst()
}
@@ -247,78 +249,89 @@ NPanel {
root.selectLast()
}
+ function selectNextWrapped() {
+ root.selectNextWrapped()
+ }
+
+ function selectPreviousWrapped() {
+ root.selectPreviousWrapped()
+ }
+
function activate() {
root.activate()
}
- ColumnLayout {
+ NBox {
anchors.fill: parent
- anchors.topMargin: Style.marginM
- anchors.leftMargin: Style.marginM
- anchors.rightMargin: Style.marginM
- anchors.bottomMargin: Style.marginS
- spacing: Style.marginXS
+ anchors.margins: Style.marginL
- // Header with title and close button
- RowLayout {
- Layout.fillWidth: true
- Layout.preferredHeight: Style.baseWidgetSize * 0.6
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Style.marginL
+ spacing: Style.marginL
- NText {
- text: timerActive ? I18n.tr("session-menu.action-in-seconds", {
- "action": pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1),
- "seconds": Math.ceil(timeRemaining / 1000)
- }) : I18n.tr("session-menu.title")
- font.weight: Style.fontWeightBold
- pointSize: Style.fontSizeM
- color: timerActive ? Color.mPrimary : Color.mOnSurface
- Layout.alignment: Qt.AlignVCenter
- verticalAlignment: Text.AlignVCenter
- }
-
- Item {
+ // Header with title and close button
+ RowLayout {
Layout.fillWidth: true
- }
+ Layout.preferredHeight: Style.baseWidgetSize * 0.6
- NIconButton {
- icon: timerActive ? "stop" : "close"
- tooltipText: timerActive ? I18n.tr("tooltips.cancel-timer") : I18n.tr("tooltips.close")
- Layout.alignment: Qt.AlignVCenter
- colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : Color.transparent
- colorFg: timerActive ? Color.mError : Color.mOnSurface
- onClicked: {
- if (timerActive) {
- cancelTimer()
- } else {
- cancelTimer()
- root.close()
+ NText {
+ text: timerActive ? I18n.tr("session-menu.action-in-seconds", {
+ "action": pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1),
+ "seconds": Math.ceil(timeRemaining / 1000)
+ }) : I18n.tr("session-menu.title")
+ font.weight: Style.fontWeightBold
+ pointSize: Style.fontSizeM
+ color: timerActive ? Color.mPrimary : Color.mOnSurface
+ Layout.alignment: Qt.AlignVCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ NIconButton {
+ icon: timerActive ? "stop" : "close"
+ tooltipText: timerActive ? I18n.tr("tooltips.cancel-timer") : I18n.tr("tooltips.close")
+ Layout.alignment: Qt.AlignVCenter
+ baseSize: Style.baseWidgetSize * 0.7
+ colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : Color.transparent
+ colorFg: timerActive ? Color.mError : Color.mOnSurface
+ onClicked: {
+ if (timerActive) {
+ cancelTimer()
+ } else {
+ cancelTimer()
+ root.close()
+ }
}
}
}
- }
- NDivider {
- Layout.fillWidth: true
- }
+ NDivider {
+ Layout.fillWidth: true
+ }
- // Power options
- ColumnLayout {
- Layout.fillWidth: true
- spacing: Style.marginS
+ // Power options
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
- Repeater {
- model: powerOptions
- delegate: PowerButton {
- Layout.fillWidth: true
- icon: modelData.icon
- title: modelData.title
- isShutdown: modelData.isShutdown || false
- isSelected: index === selectedIndex
- onClicked: {
- selectedIndex = index
- startTimer(modelData.action)
+ Repeater {
+ model: powerOptions
+ delegate: PowerButton {
+ Layout.fillWidth: true
+ icon: modelData.icon
+ title: modelData.title
+ isShutdown: modelData.isShutdown || false
+ isSelected: index === selectedIndex
+ onClicked: {
+ selectedIndex = index
+ startTimer(modelData.action)
+ }
+ pending: timerActive && pendingAction === modelData.action
}
- pending: timerActive && pendingAction === modelData.action
}
}
}
@@ -337,7 +350,7 @@ NPanel {
signal clicked
- height: Style.baseWidgetSize * 1.2 * Style.uiScaleRatio
+ height: Style.baseWidgetSize * 1.3 * Style.uiScaleRatio
radius: Style.radiusS
color: {
if (pending) {
diff --git a/Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml b/Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml
index 643684dd..514733dd 100644
--- a/Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml
+++ b/Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml
@@ -17,7 +17,8 @@ ColumnLayout {
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
property string valueHideMode: "hidden" // Default to 'Hide When Empty'
property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode
- property int valueWidth: widgetData.width !== undefined ? widgetData.width : widgetMetadata.width
+ property int valueMaxWidth: widgetData.maxWidth !== undefined ? widgetData.maxWidth : widgetMetadata.maxWidth
+ property bool valueUseFixedWidth: widgetData.useFixedWidth !== undefined ? widgetData.useFixedWidth : widgetMetadata.useFixedWidth
property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons
Component.onCompleted: {
@@ -31,7 +32,8 @@ ColumnLayout {
settings.hideMode = valueHideMode
settings.showIcon = valueShowIcon
settings.scrollingMode = valueScrollingMode
- settings.width = parseInt(widthInput.text) || widgetMetadata.width
+ settings.maxWidth = parseInt(widthInput.text) || widgetMetadata.maxWidth
+ settings.useFixedWidth = valueUseFixedWidth
settings.colorizeIcons = valueColorizeIcons
return settings
}
@@ -73,10 +75,18 @@ ColumnLayout {
NTextInput {
id: widthInput
Layout.fillWidth: true
- label: I18n.tr("bar.widget-settings.active-window.width.label")
- description: I18n.tr("bar.widget-settings.active-window.width.description")
- placeholderText: widgetMetadata.width
- text: valueWidth
+ label: I18n.tr("bar.widget-settings.active-window.max-width.label")
+ description: I18n.tr("bar.widget-settings.active-window.max-width.description")
+ placeholderText: widgetMetadata.maxWidth
+ text: valueMaxWidth
+ }
+
+ NToggle {
+ Layout.fillWidth: true
+ label: I18n.tr("bar.widget-settings.active-window.use-fixed-width.label")
+ description: I18n.tr("bar.widget-settings.active-window.use-fixed-width.description")
+ checked: valueUseFixedWidth
+ onToggled: checked => valueUseFixedWidth = checked
}
NComboBox {
diff --git a/Modules/Settings/Bar/WidgetSettings/ControlCenterSettings.qml b/Modules/Settings/Bar/WidgetSettings/ControlCenterSettings.qml
index 2c0383e2..66c30c11 100644
--- a/Modules/Settings/Bar/WidgetSettings/ControlCenterSettings.qml
+++ b/Modules/Settings/Bar/WidgetSettings/ControlCenterSettings.qml
@@ -31,7 +31,7 @@ ColumnLayout {
label: I18n.tr("bar.widget-settings.control-center.use-distro-logo.label")
description: I18n.tr("bar.widget-settings.control-center.use-distro-logo.description")
checked: valueUseDistroLogo
- onToggled: {
+ onToggled: function (checked) {
valueUseDistroLogo = checked
if (checked) {
valueCustomIconPath = ""
diff --git a/Modules/Settings/Bar/WidgetSettings/MediaMiniSettings.qml b/Modules/Settings/Bar/WidgetSettings/MediaMiniSettings.qml
index 1d03c87f..5e109feb 100644
--- a/Modules/Settings/Bar/WidgetSettings/MediaMiniSettings.qml
+++ b/Modules/Settings/Bar/WidgetSettings/MediaMiniSettings.qml
@@ -19,6 +19,8 @@ ColumnLayout {
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
property string valueVisualizerType: widgetData.visualizerType || widgetMetadata.visualizerType
property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode
+ property int valueMaxWidth: widgetData.maxWidth !== undefined ? widgetData.maxWidth : widgetMetadata.maxWidth
+ property bool valueUseFixedWidth: widgetData.useFixedWidth !== undefined ? widgetData.useFixedWidth : widgetMetadata.useFixedWidth
Component.onCompleted: {
if (widgetData && widgetData.hideMode !== undefined) {
@@ -33,6 +35,8 @@ ColumnLayout {
settings.showVisualizer = valueShowVisualizer
settings.visualizerType = valueVisualizerType
settings.scrollingMode = valueScrollingMode
+ settings.maxWidth = parseInt(widthInput.text) || widgetMetadata.maxWidth
+ settings.useFixedWidth = valueUseFixedWidth
return settings
}
@@ -87,6 +91,22 @@ ColumnLayout {
minimumWidth: 200
}
+ NTextInput {
+ id: widthInput
+ Layout.fillWidth: true
+ label: I18n.tr("bar.widget-settings.media-mini.max-width.label")
+ description: I18n.tr("bar.widget-settings.media-mini.max-width.description")
+ placeholderText: widgetMetadata.maxWidth
+ text: valueMaxWidth
+ }
+
+ NToggle {
+ label: I18n.tr("bar.widget-settings.media-mini.use-fixed-width.label")
+ description: I18n.tr("bar.widget-settings.media-mini.use-fixed-width.description")
+ checked: valueUseFixedWidth
+ onToggled: checked => valueUseFixedWidth = checked
+ }
+
NComboBox {
label: I18n.tr("bar.widget-settings.media-mini.scrolling-mode.label")
description: I18n.tr("bar.widget-settings.media-mini.scrolling-mode.description")
diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/Settings/Tabs/BarTab.qml
index e1a9421a..a9552659 100644
--- a/Modules/Settings/Tabs/BarTab.qml
+++ b/Modules/Settings/Tabs/BarTab.qml
@@ -344,14 +344,14 @@ ColumnLayout {
newArray.splice(toIndex, 0, item)
Settings.data.bar.widgets[section] = newArray
- //Logger.log("BarTab", "Widget reordered. New array:", JSON.stringify(newArray))
+ //Logger.i("BarTab", "Widget reordered. New array:", JSON.stringify(newArray))
}
}
function _updateWidgetSettingsInSection(section, index, settings) {
// Update the widget settings in the Settings data
Settings.data.bar.widgets[section][index] = settings
- //Logger.log("BarTab", `Updated widget settings for ${settings.id} in ${section} section`)
+ //Logger.i("BarTab", `Updated widget settings for ${settings.id} in ${section} section`)
}
function _moveWidgetBetweenSections(fromSection, index, toSection) {
@@ -369,7 +369,7 @@ ColumnLayout {
targetArray.push(widget)
Settings.data.bar.widgets[toSection] = targetArray
- //Logger.log("BarTab", `Moved widget ${widget.id} from ${fromSection} to ${toSection}`)
+ //Logger.i("BarTab", `Moved widget ${widget.id} from ${fromSection} to ${toSection}`)
}
}
diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml
index b38d521e..7cfcf595 100644
--- a/Modules/Settings/Tabs/ColorSchemeTab.qml
+++ b/Modules/Settings/Tabs/ColorSchemeTab.qml
@@ -13,6 +13,24 @@ ColumnLayout {
property var schemeColorsCache: ({})
property int cacheVersion: 0 // Increment to trigger UI updates
+ // Time dropdown options (00:00 .. 23:30)
+ ListModel {
+ id: timeOptions
+ }
+ Component.onCompleted: {
+ for (var h = 0; h < 24; h++) {
+ for (var m = 0; m < 60; m += 30) {
+ var hh = ("0" + h).slice(-2)
+ var mm = ("0" + m).slice(-2)
+ var key = hh + ":" + mm
+ timeOptions.append({
+ "key": key,
+ "name": key
+ })
+ }
+ }
+ }
+
spacing: Style.marginL
// Helper function to extract scheme name from path
@@ -121,7 +139,7 @@ ColumnLayout {
var jsonData = JSON.parse(text())
root.schemeLoaded(schemeName, jsonData)
} catch (e) {
- Logger.warn("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e)
+ Logger.w("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e)
root.schemeLoaded(schemeName, null)
}
}
@@ -138,8 +156,8 @@ ColumnLayout {
// Dark Mode Toggle
NToggle {
- label: I18n.tr("settings.color-scheme.color-source.dark-mode.label")
- description: I18n.tr("settings.color-scheme.color-source.dark-mode.description")
+ label: I18n.tr("settings.color-scheme.dark-mode.switch.label")
+ description: I18n.tr("settings.color-scheme.dark-mode.switch.description")
checked: Settings.data.colorSchemes.darkMode
enabled: true
onToggled: checked => {
@@ -148,6 +166,77 @@ ColumnLayout {
}
}
+ NComboBox {
+ label: I18n.tr("settings.color-scheme.dark-mode.mode.label")
+ description: I18n.tr("settings.color-scheme.dark-mode.mode.description")
+
+ model: [{
+ "name": I18n.tr("settings.color-scheme.dark-mode.mode.off"),
+ "key": "off"
+ }, {
+ "name": I18n.tr("settings.color-scheme.dark-mode.mode.manual"),
+ "key": "manual"
+ }, {
+ "name": I18n.tr("settings.color-scheme.dark-mode.mode.location"),
+ "key": "location"
+ }]
+
+ currentKey: Settings.data.colorSchemes.schedulingMode
+
+ onSelected: key => {
+ Settings.data.colorSchemes.schedulingMode = key
+ AppThemeService.generate()
+ }
+ }
+
+ // Manual scheduling
+ ColumnLayout {
+ spacing: Style.marginS
+ visible: Settings.data.colorSchemes.schedulingMode === "manual"
+
+ NLabel {
+ label: I18n.tr("settings.display.night-light.manual-schedule.label")
+ description: I18n.tr("settings.display.night-light.manual-schedule.description")
+ }
+
+ RowLayout {
+ Layout.fillWidth: false
+ spacing: Style.marginS
+
+ NText {
+ text: I18n.tr("settings.display.night-light.manual-schedule.sunrise")
+ pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ }
+
+ NComboBox {
+ model: timeOptions
+ currentKey: Settings.data.colorSchemes.manualSunrise
+ placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-start")
+ onSelected: key => Settings.data.colorSchemes.manualSunrise = key
+ minimumWidth: 120
+ }
+
+ Item {
+ Layout.preferredWidth: 20
+ }
+
+ NText {
+ text: I18n.tr("settings.display.night-light.manual-schedule.sunset")
+ pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ }
+
+ NComboBox {
+ model: timeOptions
+ currentKey: Settings.data.colorSchemes.manualSunset
+ placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-stop")
+ onSelected: key => Settings.data.colorSchemes.manualSunset = key
+ minimumWidth: 120
+ }
+ }
+ }
+
// Use Wallpaper Colors
NToggle {
label: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label")
@@ -312,7 +401,7 @@ ColumnLayout {
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.data.colorSchemes.useWallpaperColors = false
- Logger.log("ColorSchemeTab", "Disabled wallpaper colors")
+ Logger.i("ColorSchemeTab", "Disabled wallpaper colors")
Settings.data.colorSchemes.predefinedScheme = schemeItem.schemeName
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
@@ -336,7 +425,6 @@ ColumnLayout {
NIcon {
icon: "check"
pointSize: Style.fontSizeXS
- font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
@@ -514,29 +602,49 @@ ColumnLayout {
}
}
- // Show individual checkboxes for each detected Discord client
- Repeater {
- model: ProgramCheckerService.availableDiscordClients
- delegate: NCheckbox {
- label: modelData.name.charAt(0).toUpperCase() + modelData.name.slice(1)
- description: I18n.tr("settings.color-scheme.templates.programs.discord.description", {
- "client": modelData.name.charAt(0).toUpperCase() + modelData.name.slice(1),
- "filepath": modelData.themePath
- })
- checked: Settings.data.templates["discord_" + modelData.name] || false
- onToggled: checked => {
- Settings.data.templates["discord_" + modelData.name] = checked
+ // Discord clients - single toggle with dynamic description
+ NCheckbox {
+ id: discordToggle
+ label: "Discord"
+ description: {
+ if (ProgramCheckerService.availableDiscordClients.length === 0) {
+ return I18n.tr("settings.color-scheme.templates.programs.discord.description-missing")
+ } else {
+ // Show detected clients
+ var clientInfo = []
+ for (var i = 0; i < ProgramCheckerService.availableDiscordClients.length; i++) {
+ var client = ProgramCheckerService.availableDiscordClients[i]
+ clientInfo.push(client.name.charAt(0).toUpperCase() + client.name.slice(1))
+ }
+ return "Detected: " + clientInfo.join(", ")
+ }
+ }
+ Layout.fillWidth: true
+ Layout.preferredWidth: -1
+ checked: {
+ // Check if any Discord client template is enabled
+ var anyEnabled = false
+ for (var i = 0; i < ProgramCheckerService.availableDiscordClients.length; i++) {
+ var client = ProgramCheckerService.availableDiscordClients[i]
+ if (Settings.data.templates["discord_" + client.name]) {
+ anyEnabled = true
+ break
+ }
+ }
+ return anyEnabled
+ }
+ enabled: ProgramCheckerService.availableDiscordClients.length > 0
+ opacity: ProgramCheckerService.availableDiscordClients.length > 0 ? 1.0 : 0.6
+ onToggled: checked => {
+ // Enable/disable all detected Discord clients
+ for (var i = 0; i < ProgramCheckerService.availableDiscordClients.length; i++) {
+ var client = ProgramCheckerService.availableDiscordClients[i]
+ Settings.data.templates["discord_" + client.name] = checked
+ }
+ if (ProgramCheckerService.availableDiscordClients.length > 0) {
AppThemeService.generate()
}
- }
- }
-
- // Show message if no Discord clients detected
- NText {
- visible: ProgramCheckerService.availableDiscordClients.length === 0
- text: I18n.tr("settings.color-scheme.templates.programs.discord.description-missing")
- color: Color.mOnSurfaceVariant
- pointSize: Style.fontSizeS
+ }
}
NCheckbox {
@@ -571,6 +679,9 @@ ColumnLayout {
checked: Settings.data.templates.enableUserTemplates
onToggled: checked => {
Settings.data.templates.enableUserTemplates = checked
+ if (checked) {
+ MatugenTemplates.writeUserTemplatesToml()
+ }
AppThemeService.generate()
}
}
diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Settings/Tabs/ControlCenterTab.qml
index 4bf62cfb..372add57 100644
--- a/Modules/Settings/Tabs/ControlCenterTab.qml
+++ b/Modules/Settings/Tabs/ControlCenterTab.qml
@@ -143,6 +143,9 @@ ColumnLayout {
}, {
"key": "top_center",
"name": I18n.tr("options.control-center.position.top_center")
+ }, {
+ "key": "center",
+ "name": I18n.tr("options.control-center.position.center")
}]
currentKey: Settings.data.controlCenter.position
onSelected: function (key) {
@@ -177,7 +180,7 @@ ColumnLayout {
root.handleDragEnd()
}
onItemToggled: function (index, enabled) {
- //Logger.log("ControlCenterTab", "Item", index, "toggled to", enabled)
+ //Logger.i("ControlCenterTab", "Item", index, "toggled to", enabled)
var newModel = cardsModel.slice()
newModel[index] = Object.assign({}, newModel[index], {
"enabled": enabled
@@ -186,7 +189,7 @@ ColumnLayout {
saveCards()
}
onItemsReordered: function (fromIndex, toIndex) {
- //Logger.log("ControlCenterTab", "Item moved from", fromIndex, "to", toIndex)
+ //Logger.i("ControlCenterTab", "Item moved from", fromIndex, "to", toIndex)
var newModel = cardsModel.slice()
var item = newModel.splice(fromIndex, 1)[0]
newModel.splice(toIndex, 0, item)
@@ -321,7 +324,7 @@ ColumnLayout {
targetArray.push(widget)
Settings.data.controlCenter.shortcuts[toSection] = targetArray
- //Logger.log("BarTab", `Moved widget ${widget.id} from ${fromSection} to ${toSection}`)
+ //Logger.i("BarTab", `Moved widget ${widget.id} from ${fromSection} to ${toSection}`)
}
}
diff --git a/Modules/Settings/Tabs/DockTab.qml b/Modules/Settings/Tabs/DockTab.qml
index 3203446e..c7f1d258 100644
--- a/Modules/Settings/Tabs/DockTab.qml
+++ b/Modules/Settings/Tabs/DockTab.qml
@@ -87,6 +87,24 @@ ColumnLayout {
}
}
+ ColumnLayout {
+ spacing: Style.marginXXS
+ Layout.fillWidth: true
+ NLabel {
+ label: I18n.tr("settings.dock.appearance.icon-size.label")
+ description: I18n.tr("settings.dock.appearance.icon-size.description")
+ }
+ NValueSlider {
+ Layout.fillWidth: true
+ from: 0
+ to: 2
+ stepSize: 0.01
+ value: Settings.data.dock.size
+ onMoved: value => Settings.data.dock.size = value
+ text: Math.floor(Settings.data.dock.size * 100) + "%"
+ }
+ }
+
NToggle {
label: I18n.tr("settings.dock.monitors.only-same-output.label")
description: I18n.tr("settings.dock.monitors.only-same-output.description")
diff --git a/Modules/Settings/Tabs/GeneralTab.qml b/Modules/Settings/Tabs/GeneralTab.qml
index c4554145..40b5276d 100644
--- a/Modules/Settings/Tabs/GeneralTab.qml
+++ b/Modules/Settings/Tabs/GeneralTab.qml
@@ -189,4 +189,63 @@ ColumnLayout {
Layout.topMargin: Style.marginXL
Layout.bottomMargin: Style.marginXL
}
+
+ // Language selection
+ ColumnLayout {
+ spacing: Style.marginL
+ Layout.fillWidth: true
+
+ NHeader {
+ label: I18n.tr("settings.general.language.section.label")
+ description: I18n.tr("settings.general.language.section.description")
+ }
+
+ NComboBox {
+ Layout.fillWidth: true
+ label: I18n.tr("settings.general.language.select.label")
+ description: I18n.tr("settings.general.language.select.description")
+ model: [
+ { "key": "", "name": I18n.tr("settings.general.language.select.auto-detect") + " (" + I18n.systemDetectedLangCode + ")" }
+ ].concat(I18n.availableLanguages.map(function(langCode) {
+ return { "key": langCode, "name": langCode }
+ }))
+ currentKey: Settings.data.general.language
+ onSelected: key => {
+ Settings.data.general.language = key
+ if (key === "") {
+ I18n.detectLanguage() // Re-detect system language if "Automatic" is selected
+ } else {
+ I18n.setLanguage(key) // Set specific language
+ }
+ }
+ }
+ }
+
+ NDivider {
+ Layout.fillWidth: true
+ Layout.topMargin: Style.marginXL
+ Layout.bottomMargin: Style.marginXL
+ }
+ ColumnLayout {
+ spacing: Style.marginL
+ Layout.fillWidth: true
+
+ NHeader {
+ label: I18n.tr("settings.general.lockscreen.section.label")
+ description: I18n.tr("settings.general.lockscreen.section.description")
+ }
+
+ NToggle {
+ label: I18n.tr("settings.general.lockscreen.lock-on-suspend.label")
+ description: I18n.tr("settings.general.lockscreen.lock-on-suspend.description")
+ checked: Settings.data.general.lockOnSuspend
+ onToggled: Settings.data.general.lockOnSuspend = checked
+ }
+ }
+
+ NDivider {
+ Layout.fillWidth: true
+ Layout.topMargin: Style.marginXL
+ Layout.bottomMargin: Style.marginXL
+ }
}
diff --git a/Modules/Settings/Tabs/LauncherTab.qml b/Modules/Settings/Tabs/LauncherTab.qml
index b64f26b6..88dfb6be 100644
--- a/Modules/Settings/Tabs/LauncherTab.qml
+++ b/Modules/Settings/Tabs/LauncherTab.qml
@@ -95,8 +95,14 @@ ColumnLayout {
NToggle {
label: I18n.tr("settings.launcher.settings.use-app2unit.label")
description: I18n.tr("settings.launcher.settings.use-app2unit.description")
- checked: Settings.data.appLauncher.useApp2Unit
- onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
+ checked: Settings.data.appLauncher.useApp2Unit && ProgramCheckerService.app2unitAvailable
+ enabled: ProgramCheckerService.app2unitAvailable
+ opacity: ProgramCheckerService.app2unitAvailable ? 1.0 : 0.6
+ onToggled: checked => {
+ if (ProgramCheckerService.app2unitAvailable) {
+ Settings.data.appLauncher.useApp2Unit = checked
+ }
+ }
}
NTextInput {
diff --git a/Modules/Settings/Tabs/LocationTab.qml b/Modules/Settings/Tabs/LocationTab.qml
index b4ccfe4f..7b6c340b 100644
--- a/Modules/Settings/Tabs/LocationTab.qml
+++ b/Modules/Settings/Tabs/LocationTab.qml
@@ -71,11 +71,20 @@ ColumnLayout {
description: I18n.tr("settings.location.weather.section.description")
}
+ NToggle {
+ label: I18n.tr("settings.location.weather.enabled.label")
+ description: I18n.tr("settings.location.weather.enabled.description")
+ checked: Settings.data.location.weatherEnabled
+ onToggled: checked => Settings.data.location.weatherEnabled = checked
+ }
+
NToggle {
label: I18n.tr("settings.location.weather.fahrenheit.label")
description: I18n.tr("settings.location.weather.fahrenheit.description")
checked: Settings.data.location.useFahrenheit
onToggled: checked => Settings.data.location.useFahrenheit = checked
+ enabled: Settings.data.location.weatherEnabled
+ opacity: Settings.data.location.weatherEnabled ? 1.0 : 0.5
}
}
diff --git a/Modules/SetupWizard/SetupAppearanceStep.qml b/Modules/SetupWizard/SetupAppearanceStep.qml
new file mode 100644
index 00000000..e247e070
--- /dev/null
+++ b/Modules/SetupWizard/SetupAppearanceStep.qml
@@ -0,0 +1,557 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell.Io
+import qs.Commons
+import qs.Services
+import qs.Widgets
+
+ColumnLayout {
+ id: root
+ spacing: Style.marginM
+
+ function extractSchemeName(path) {
+ var basename = path.split('/').pop()
+ return basename.replace('.json', '')
+ }
+
+ // Cache for scheme colors (mirrors ColorSchemeTab approach)
+ property var schemeColorsCache: ({})
+ property int cacheVersion: 0
+
+ function getSchemeColor(schemeName, key) {
+ try {
+ var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
+ var data = schemeColorsCache[schemeName]
+ if (data && data[mode] && data[mode][key])
+ return data[mode][key]
+ } catch (e) {
+
+ }
+ return Color.mSurfaceVariant
+ }
+
+ // Match ColorSchemeTab helpers
+ function schemeLoaded(schemeName, jsonData) {
+ var value = jsonData || {}
+ schemeColorsCache[schemeName] = value
+ cacheVersion++
+ Logger.i("SetupAppearanceStep", `Loaded scheme ${schemeName}`)
+ }
+
+ Connections {
+ target: ColorSchemeService
+ function onSchemesChanged() {
+ Logger.i("SetupAppearanceStep", `Color schemes changed: ${ColorSchemeService.schemes.length}`)
+ schemeColorsCache = {}
+ cacheVersion++
+ }
+ }
+
+ // Beautiful header with icon
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.bottomMargin: Style.marginL
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: Style.radiusL
+ color: Color.mSurfaceVariant
+ opacity: 0.6
+
+ NIcon {
+ icon: "palette"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginXS
+
+ NText {
+ text: I18n.tr("setup.appearance.header")
+ pointSize: Style.fontSizeXL
+ font.weight: Style.fontWeightBold
+ color: Color.mPrimary
+ }
+
+ NText {
+ text: I18n.tr("setup.appearance.subheader")
+ pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ ScrollView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ contentWidth: availableWidth
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+ ScrollBar.vertical.policy: ScrollBar.AsNeeded
+
+ ColumnLayout {
+ width: parent.width
+ spacing: Style.marginM
+
+ // Dark Mode Toggle
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+
+ NIcon {
+ icon: "moon"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.dark-mode.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.dark-mode.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+
+ NToggle {
+ checked: Settings.data.colorSchemes.darkMode
+ onToggled: checked => Settings.data.colorSchemes.darkMode = checked
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // Wallpaper Colors Toggle
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+
+ NIcon {
+ icon: "color-picker"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+
+ NToggle {
+ enabled: ProgramCheckerService.matugenAvailable
+ opacity: ProgramCheckerService.matugenAvailable ? 1.0 : 0.6
+ checked: Settings.data.colorSchemes.useWallpaperColors && ProgramCheckerService.matugenAvailable
+ onToggled: checked => {
+ if (!ProgramCheckerService.matugenAvailable)
+ return
+ if (checked) {
+ Settings.data.colorSchemes.useWallpaperColors = true
+ AppThemeService.generate()
+ } else {
+ Settings.data.colorSchemes.useWallpaperColors = false
+ if (Settings.data.colorSchemes.predefinedScheme) {
+ ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
+ }
+ }
+ }
+ }
+ }
+
+ // Matugen not available notice
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+ visible: !ProgramCheckerService.matugenAvailable
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+ NIcon {
+ icon: "alert-triangle"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description")
+ // Reuse description; availability is visually indicated
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+
+ // Matugen scheme type (visible when wallpaper colors enabled and matugen available)
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+ visible: Settings.data.colorSchemes.useWallpaperColors && ProgramCheckerService.matugenAvailable
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+
+ NIcon {
+ icon: "wand"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.matugen-scheme-type.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.matugen-scheme-type.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ // Matugen scheme options styled like bar position buttons
+ GridLayout {
+ Layout.fillWidth: true
+ columns: 2
+ rowSpacing: Style.marginS
+ columnSpacing: Style.marginS
+
+ Repeater {
+ model: [{
+ "key": "scheme-content",
+ "name": "Content"
+ }, {
+ "key": "scheme-expressive",
+ "name": "Expressive"
+ }, {
+ "key": "scheme-fidelity",
+ "name": "Fidelity"
+ }, {
+ "key": "scheme-fruit-salad",
+ "name": "Fruit Salad"
+ }, {
+ "key": "scheme-monochrome",
+ "name": "Monochrome"
+ }, {
+ "key": "scheme-neutral",
+ "name": "Neutral"
+ }, {
+ "key": "scheme-rainbow",
+ "name": "Rainbow"
+ }, {
+ "key": "scheme-tonal-spot",
+ "name": "Tonal Spot"
+ }]
+ delegate: Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 48
+ radius: Style.radiusM
+ border.width: 1
+
+ property bool isActive: Settings.data.colorSchemes.matugenSchemeType === modelData.key
+
+ color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mSurfaceVariant
+ border.color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mOutline
+ opacity: (hoverHandler.hovered || isActive) ? 1.0 : 0.8
+
+ NText {
+ text: modelData.name
+ pointSize: Style.fontSizeM
+ font.weight: (hoverHandler.hovered || parent.isActive) ? Style.fontWeightBold : Style.fontWeightMedium
+ color: (hoverHandler.hovered || parent.isActive) ? Color.mOnPrimary : Color.mOnSurface
+ anchors.centerIn: parent
+ }
+
+ HoverHandler {
+ id: hoverHandler
+ }
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ Settings.data.colorSchemes.matugenSchemeType = modelData.key
+ AppThemeService.generate()
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ visible: !Settings.data.colorSchemes.useWallpaperColors
+ }
+
+ // Predefined schemes section (visible when wallpaper colors disabled)
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+ visible: !Settings.data.colorSchemes.useWallpaperColors
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+
+ NIcon {
+ icon: "palette"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ NText {
+ text: I18n.tr("settings.color-scheme.predefined.section.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+
+ NText {
+ text: I18n.tr("settings.color-scheme.predefined.section.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ // Predefined schemes Grid (matches ColorSchemeTab)
+ GridLayout {
+ id: schemesGrid
+ columns: Math.max(2, Math.floor((parent.width - Style.marginM * 2) / 180))
+ rowSpacing: Style.marginM
+ columnSpacing: Style.marginM
+ Layout.fillWidth: true
+
+ Repeater {
+ model: ColorSchemeService.schemes
+
+ delegate: Rectangle {
+ id: schemeItem
+
+ property string schemePath: modelData
+ property string schemeName: root.extractSchemeName(modelData)
+
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter
+ height: 50
+ radius: Style.radiusS
+ color: root.cacheVersion >= 0 ? root.getSchemeColor(schemeName, "mSurface") : root.getSchemeColor(schemeName, "mSurface")
+ border.width: Math.max(1, Style.borderL)
+ border.color: itemMouseArea.containsMouse ? Color.mTertiary : (Settings.data.colorSchemes.predefinedScheme === schemeName ? Color.mSecondary : Color.mOutline)
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Style.marginL
+ spacing: Style.marginXS
+
+ NText {
+ text: schemeItem.schemeName
+ pointSize: Style.fontSizeS
+ font.weight: Style.fontWeightMedium
+ color: Color.mOnSurface
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ verticalAlignment: Text.AlignVCenter
+ wrapMode: Text.WordWrap
+ maximumLineCount: 1
+ }
+
+ Rectangle {
+ width: 14
+ height: 14
+ radius: width * 0.5
+ color: root.cacheVersion >= 0 ? (function () {
+ var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
+ var cached = root.schemeColorsCache[schemeItem.schemeName]
+ return (cached && cached[mode] && cached[mode].mPrimary) || root.getSchemeColor(schemeItem.schemeName, "mPrimary")
+ })() : Color.mPrimary
+ }
+ Rectangle {
+ width: 14
+ height: 14
+ radius: width * 0.5
+ color: root.cacheVersion >= 0 ? (function () {
+ var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
+ var cached = root.schemeColorsCache[schemeItem.schemeName]
+ return (cached && cached[mode] && cached[mode].mSecondary) || root.getSchemeColor(schemeItem.schemeName, "mSecondary")
+ })() : Color.mSecondary
+ }
+ Rectangle {
+ width: 14
+ height: 14
+ radius: width * 0.5
+ color: root.cacheVersion >= 0 ? (function () {
+ var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
+ var cached = root.schemeColorsCache[schemeItem.schemeName]
+ return (cached && cached[mode] && cached[mode].mTertiary) || root.getSchemeColor(schemeItem.schemeName, "mTertiary")
+ })() : Color.mTertiary
+ }
+ Rectangle {
+ width: 14
+ height: 14
+ radius: width * 0.5
+ color: root.cacheVersion >= 0 ? (function () {
+ var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
+ var cached = root.schemeColorsCache[schemeItem.schemeName]
+ return (cached && cached[mode] && cached[mode].mError) || root.getSchemeColor(schemeItem.schemeName, "mError")
+ })() : Color.mError
+ }
+ }
+
+ MouseArea {
+ id: itemMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ Settings.data.colorSchemes.useWallpaperColors = false
+ Settings.data.colorSchemes.predefinedScheme = schemeItem.schemeName
+ ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Bottom spacer
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Style.marginL
+ }
+ }
+ }
+
+ // Hidden loader to populate schemeColorsCache from files
+ Item {
+ visible: false
+ Repeater {
+ model: ColorSchemeService.schemes
+ delegate: Item {
+ FileView {
+ path: modelData
+ blockLoading: false
+ onLoaded: {
+ var schemeName = root.extractSchemeName(path)
+ try {
+ var jsonData = JSON.parse(text())
+ root.schemeLoaded(schemeName, jsonData)
+ } catch (e) {
+ root.schemeLoaded(schemeName, null)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Modules/SetupWizard/SetupCustomizeStep.qml b/Modules/SetupWizard/SetupCustomizeStep.qml
new file mode 100644
index 00000000..e5e0dcc1
--- /dev/null
+++ b/Modules/SetupWizard/SetupCustomizeStep.qml
@@ -0,0 +1,508 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell
+import qs.Commons
+import qs.Services
+import qs.Widgets
+
+ColumnLayout {
+ id: root
+
+ property real selectedScaleRatio: 1.0
+ property string selectedBarPosition: "top"
+ property bool selectedDimDesktop: true
+
+ signal scaleRatioChanged(real ratio)
+ signal barPositionChanged(string position)
+ signal dimDesktopChanged(bool dim)
+
+ spacing: Style.marginM
+
+ // Beautiful header with icon
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.bottomMargin: Style.marginL
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: Style.radiusL
+ color: Color.mSurfaceVariant
+ opacity: 0.6
+
+ NIcon {
+ icon: "palette"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginXS
+
+ NText {
+ text: I18n.tr("setup.customize.header")
+ pointSize: Style.fontSizeXL
+ font.weight: Style.fontWeightBold
+ color: Color.mPrimary
+ }
+
+ NText {
+ text: I18n.tr("setup.customize.subheader")
+ pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ ScrollView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ contentWidth: availableWidth
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+ ScrollBar.vertical.policy: ScrollBar.AsNeeded
+
+ ColumnLayout {
+ width: parent.width
+ spacing: Style.marginM
+
+ // Bar Position section
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+
+ NIcon {
+ icon: "layout-2"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ NText {
+ text: I18n.tr("settings.bar.appearance.position.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+
+ NText {
+ text: I18n.tr("settings.bar.appearance.position.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ Repeater {
+ model: [{
+ "key": "top",
+ "name": I18n.tr("options.bar.position.top"),
+ "icon": "arrow-up"
+ }, {
+ "key": "bottom",
+ "name": I18n.tr("options.bar.position.bottom"),
+ "icon": "arrow-down"
+ }, {
+ "key": "left",
+ "name": I18n.tr("options.bar.position.left"),
+ "icon": "arrow-left"
+ }, {
+ "key": "right",
+ "name": I18n.tr("options.bar.position.right"),
+ "icon": "arrow-right"
+ }]
+ delegate: Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 40
+ radius: Style.radiusM
+ border.width: 1
+
+ property bool isActive: selectedBarPosition === modelData.key
+
+ color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mSurfaceVariant
+ border.color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mOutline
+ opacity: (hoverHandler.hovered || isActive) ? 1.0 : 0.8
+
+ NText {
+ text: modelData.name
+ pointSize: Style.fontSizeM
+ font.weight: (hoverHandler.hovered || parent.isActive) ? Style.fontWeightBold : Style.fontWeightMedium
+ color: (hoverHandler.hovered || parent.isActive) ? Color.mOnPrimary : Color.mOnSurface
+ anchors.centerIn: parent
+ }
+
+ HoverHandler {
+ id: hoverHandler
+ }
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ selectedBarPosition = modelData.key
+ barPositionChanged(modelData.key)
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // Dim Desktop section
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 32
+ height: 32
+ radius: Style.radiusM
+ color: Color.mSurface
+ NIcon {
+ icon: "moon"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+ NText {
+ text: I18n.tr("settings.user-interface.dim-desktop.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+ NText {
+ text: I18n.tr("settings.user-interface.dim-desktop.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+
+ NToggle {
+ checked: selectedDimDesktop
+ onToggled: function (checked) {
+ selectedDimDesktop = checked
+ dimDesktopChanged(checked)
+ }
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // Bar Density section
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 32
+ height: 32
+ radius: Style.radiusM
+ color: Color.mSurface
+ NIcon {
+ icon: "minimize"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+ NText {
+ text: I18n.tr("settings.bar.appearance.density.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+ NText {
+ text: I18n.tr("settings.bar.appearance.density.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+
+ RowLayout {
+ spacing: Style.marginS
+ Repeater {
+ model: [{
+ "key": "mini",
+ "name": I18n.tr("options.bar.density.mini")
+ }, {
+ "key": "compact",
+ "name": I18n.tr("options.bar.density.compact")
+ }, {
+ "key": "default",
+ "name": I18n.tr("options.bar.density.default")
+ }, {
+ "key": "comfortable",
+ "name": I18n.tr("options.bar.density.comfortable")
+ }]
+ delegate: Rectangle {
+ radius: 16
+ border.width: 1
+ Layout.preferredHeight: 32
+ Layout.preferredWidth: Math.max(90, densityText.implicitWidth + Style.marginXL * 2)
+
+ property bool isActive: Settings.data.bar.density === modelData.key
+
+ color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mSurfaceVariant
+ border.color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mOutline
+ opacity: (hoverHandler.hovered || isActive) ? 1.0 : 0.8
+
+ NText {
+ id: densityText
+ text: modelData.name
+ pointSize: Style.fontSizeS
+ font.weight: (hoverHandler.hovered || parent.isActive) ? Style.fontWeightBold : Style.fontWeightMedium
+ color: (hoverHandler.hovered || parent.isActive) ? Color.mOnPrimary : Color.mOnSurface
+ anchors.centerIn: parent
+ }
+
+ HoverHandler {
+ id: hoverHandler
+ }
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ Settings.data.bar.density = modelData.key
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // UI Scale section
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+ Rectangle {
+ width: 32
+ height: 32
+ radius: Style.radiusM
+ color: Color.mSurface
+ NIcon {
+ icon: "maximize"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+ NText {
+ text: I18n.tr("settings.user-interface.scaling.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+ NText {
+ text: I18n.tr("settings.user-interface.scaling.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ NValueSlider {
+ Layout.fillWidth: true
+ from: 0.8
+ to: 1.2
+ stepSize: 0.05
+ value: selectedScaleRatio
+ onMoved: function (value) {
+ selectedScaleRatio = value
+ scaleRatioChanged(value)
+ }
+ text: Math.floor(selectedScaleRatio * 100) + "%"
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // Bar Floating toggle
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+ Rectangle {
+ width: 32
+ height: 32
+ radius: Style.radiusM
+ color: Color.mSurface
+ NIcon {
+ icon: "layout-2"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+ NText {
+ text: I18n.tr("settings.bar.appearance.floating.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+ NText {
+ text: I18n.tr("settings.bar.appearance.floating.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+ NToggle {
+ checked: Settings.data.bar.floating
+ onToggled: function (checked) {
+ Settings.data.bar.floating = checked
+ }
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // Bar Background Opacity
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+ NLabel {
+ label: I18n.tr("settings.bar.appearance.background-opacity.label")
+ description: I18n.tr("settings.bar.appearance.background-opacity.description")
+ }
+ NValueSlider {
+ Layout.fillWidth: true
+ from: 0
+ to: 1
+ stepSize: 0.01
+ value: Settings.data.bar.backgroundOpacity
+ onMoved: function (value) {
+ Settings.data.bar.backgroundOpacity = value
+ }
+ text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Style.marginL
+ }
+ }
+ }
+}
diff --git a/Modules/SetupWizard/SetupWallpaperStep.qml b/Modules/SetupWizard/SetupWallpaperStep.qml
new file mode 100644
index 00000000..aa915f72
--- /dev/null
+++ b/Modules/SetupWizard/SetupWallpaperStep.qml
@@ -0,0 +1,508 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Io
+import qs.Commons
+import qs.Services
+import qs.Widgets
+import "../../Helpers/FuzzySort.js" as FuzzySort
+
+ColumnLayout {
+ id: root
+
+ property string selectedDirectory: ""
+ property string selectedWallpaper: ""
+
+ signal directoryChanged(string directory)
+ signal wallpaperChanged(string wallpaper)
+
+ spacing: Style.marginL
+
+ // Beautiful header with icon
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.bottomMargin: Style.marginL
+ spacing: Style.marginM
+
+ RowLayout {
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: Style.radiusL
+ color: Color.mSurfaceVariant
+ opacity: 0.6
+
+ NIcon {
+ icon: "image"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ spacing: Style.marginXS
+
+ NText {
+ text: I18n.tr("setup.wallpaper.header")
+ pointSize: Style.fontSizeXL
+ font.weight: Style.fontWeightBold
+ color: Color.mPrimary
+ }
+
+ NText {
+ text: I18n.tr("setup.wallpaper.subheader")
+ pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+ }
+
+ // Large preview with rounded corners and shadow effect
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.minimumHeight: 180
+ color: Color.mSurfaceVariant
+ radius: Style.radiusL
+ border.color: selectedWallpaper !== "" ? Color.mPrimary : Color.mOutline
+ border.width: selectedWallpaper !== "" ? 2 : 1
+ clip: true
+
+ // Mirror WallpaperPanel approach with rounded shader mask
+ NImageCached {
+ id: previewCached
+ anchors.fill: parent
+ anchors.margins: 4
+ maxCacheDimension: 512
+ cacheFolder: Settings.cacheDirImagesWallpapers
+ imagePath: selectedWallpaper !== "" ? "file://" + selectedWallpaper : ""
+ visible: false // used as texture source for the shader
+ }
+
+ ShaderEffect {
+ anchors.fill: parent
+ anchors.margins: 4
+ property var source: ShaderEffectSource {
+ sourceItem: previewCached
+ hideSource: true
+ live: true
+ recursive: false
+ format: ShaderEffectSource.RGBA
+ }
+ property real itemWidth: width
+ property real itemHeight: height
+ property real cornerRadius: Style.radiusL
+ property real imageOpacity: 1.0
+ fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
+ supportsAtlasTextures: false
+ blending: true
+ }
+
+ // Loading placeholder
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mSurfaceVariant
+ radius: Style.radiusL
+ visible: (previewCached.status === Image.Loading || previewCached.status === Image.Null) && selectedWallpaper !== ""
+
+ NIcon {
+ icon: "image"
+ pointSize: Style.fontSizeXXL
+ color: Color.mOnSurfaceVariant
+ anchors.centerIn: parent
+ }
+ }
+
+ // Error placeholder
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mError
+ opacity: 0.1
+ radius: Style.radiusL
+ visible: previewCached.status === Image.Error && selectedWallpaper !== ""
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: Style.marginS
+
+ NIcon {
+ icon: "alert-circle"
+ pointSize: Style.fontSizeXXL
+ color: Color.mError
+ Layout.alignment: Qt.AlignHCenter
+ }
+
+ NText {
+ text: I18n.tr("setup.wallpaper.preview-error")
+ pointSize: Style.fontSizeS
+ color: Color.mError
+ Layout.alignment: Qt.AlignHCenter
+ }
+ }
+ }
+
+ NBusyIndicator {
+ anchors.centerIn: parent
+ visible: (previewCached.status === Image.Loading || previewCached.status === Image.Null) && selectedWallpaper !== ""
+ running: visible
+ size: 28
+ }
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: Style.marginL
+ visible: selectedWallpaper === ""
+ opacity: 0.6
+
+ Rectangle {
+ Layout.alignment: Qt.AlignHCenter
+ width: 64
+ height: 64
+ radius: width / 2
+ color: Color.mPrimary
+ opacity: 0.15
+
+ NIcon {
+ icon: "sparkles"
+ pointSize: Style.fontSizeXXL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ NText {
+ text: I18n.tr("setup.wallpaper.select-prompt")
+ pointSize: Style.fontSizeL
+ color: Color.mOnSurfaceVariant
+ Layout.alignment: Qt.AlignHCenter
+ font.weight: Style.fontWeightMedium
+ }
+ }
+
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ }
+
+ // Wallpaper gallery strip
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 90
+ visible: filteredWallpapers.length > 0
+
+ ScrollView {
+ anchors.fill: parent
+ clip: true
+ ScrollBar.horizontal.policy: ScrollBar.AsNeeded
+ ScrollBar.vertical.policy: ScrollBar.AlwaysOff
+
+ RowLayout {
+ spacing: Style.marginM
+ height: parent.height
+
+ Repeater {
+ model: filteredWallpapers
+ delegate: Rectangle {
+ Layout.preferredWidth: 120
+ Layout.preferredHeight: 80
+ color: Color.mSurface
+ radius: Style.radiusM
+ border.color: selectedWallpaper === modelData ? Color.mPrimary : Color.mOutline
+ border.width: selectedWallpaper === modelData ? 2 : 1
+ clip: true
+
+ // Cached thumbnail (used as shader source)
+ NImageCached {
+ id: thumbCached
+ anchors.fill: parent
+ anchors.margins: 3
+ maxCacheDimension: 256
+ cacheFolder: Settings.cacheDirImagesWallpapers
+ imagePath: "file://" + modelData
+ visible: false
+ }
+
+ ShaderEffect {
+ anchors.fill: parent
+ anchors.margins: 3
+ property var source: ShaderEffectSource {
+ sourceItem: thumbCached
+ hideSource: true
+ live: true
+ recursive: false
+ format: ShaderEffectSource.RGBA
+ }
+ property real itemWidth: width
+ property real itemHeight: height
+ property real cornerRadius: Style.radiusM - 3
+ property real imageOpacity: 1.0
+ fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
+ supportsAtlasTextures: false
+ blending: true
+ }
+
+ // Loading state
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mSurfaceVariant
+ radius: Style.radiusM
+ visible: thumbCached.status === Image.Loading
+
+ NIcon {
+ icon: "image"
+ pointSize: Style.fontSizeL
+ color: Color.mOnSurfaceVariant
+ anchors.centerIn: parent
+ }
+ }
+
+ // Error state
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mSurfaceVariant
+ radius: Style.radiusM
+ visible: thumbCached.status === Image.Error
+
+ NIcon {
+ icon: "image"
+ pointSize: Style.fontSizeL
+ color: Color.mOnSurfaceVariant
+ anchors.centerIn: parent
+ }
+ }
+
+ NBusyIndicator {
+ anchors.centerIn: parent
+ visible: thumbCached.status === Image.Loading || thumbCached.status === Image.Null
+ running: visible
+ size: 18
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mPrimary
+ opacity: hoverHandler.hovered ? 0.1 : 0
+ radius: Style.radiusM
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+ }
+ }
+
+ Rectangle {
+ visible: selectedWallpaper === modelData
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: 6
+ width: 24
+ height: 24
+ radius: width / 2
+ color: Color.mPrimary
+
+ NIcon {
+ icon: "check"
+ pointSize: Style.fontSizeS
+ color: Color.mOnPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ HoverHandler {
+ id: hoverHandler
+ }
+
+ TapHandler {
+ onTapped: {
+ selectedWallpaper = modelData
+ wallpaperChanged(modelData)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Helpful info card
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 80
+ color: Color.mSurfaceVariant
+ radius: Style.radiusM
+ opacity: 0.4
+ visible: filteredWallpapers.length === 0
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Style.marginL
+ spacing: Style.marginM
+
+ NIcon {
+ icon: "folder-open"
+ pointSize: Style.fontSizeL
+ color: Color.mOnSurfaceVariant
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginXS
+ NText {
+ text: filteredWallpapers.length === 0 && selectedDirectory !== "" ? I18n.tr("setup.wallpaper.none-in-dir") : I18n.tr("setup.wallpaper.no-dir")
+ pointSize: Style.fontSizeM
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurfaceVariant
+ }
+ NText {
+ text: selectedDirectory !== "" ? I18n.tr("setup.wallpaper.no-valid", {
+ "dir": selectedDirectory
+ }) : I18n.tr("setup.wallpaper.choose-dir")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ opacity: 0.8
+ }
+ }
+ }
+ }
+
+ // Directory selection
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ NTextInputButton {
+ id: wallpaperPathInput
+ label: I18n.tr("setup.wallpaper.dir.label")
+ description: I18n.tr("setup.wallpaper.dir.description")
+ text: selectedDirectory
+ buttonIcon: "folder-open"
+ buttonTooltip: I18n.tr("setup.wallpaper.dir.browse")
+ Layout.fillWidth: true
+ onInputEditingFinished: {
+ selectedDirectory = text
+ directoryChanged(text)
+ }
+ onButtonClicked: directoryPicker.open()
+ }
+ }
+
+ // Internal properties and functions
+ property list wallpapersList: []
+ property list filteredWallpapers: []
+
+ function updateFilteredWallpapers() {
+ filteredWallpapers = wallpapersList
+ }
+
+ function refreshWallpapers() {
+ if (!selectedDirectory || selectedDirectory === "") {
+ wallpapersList = []
+ filteredWallpapers = []
+ return
+ }
+ if (typeof WallpaperService !== "undefined" && WallpaperService.getWallpapersList) {
+ var wallpapers = WallpaperService.getWallpapersList(Screen.name)
+ wallpapersList = wallpapers
+ updateFilteredWallpapers()
+ if (wallpapersList.length > 0 && selectedWallpaper === "") {
+ selectedWallpaper = wallpapersList[0]
+ }
+ } else {
+ readDirectoryImages(selectedDirectory)
+ }
+ }
+
+ function readDirectoryImages(directoryPath) {
+ directoryScanner.command = ["find", directoryPath, "-type", "f", "\\(-iname", "*.jpg", "-o", "-iname", "*.jpeg", "-o", "-iname", "*.png", "-o", "-iname", "*.bmp", "-o", "-iname", "*.webp", "-o", "-iname", "*.svg", "\\)"]
+ directoryScanner.running = true
+ return []
+ }
+
+ onSelectedDirectoryChanged: {
+ if (typeof Settings !== "undefined" && Settings.data && Settings.data.wallpaper) {
+ Settings.data.wallpaper.directory = selectedDirectory
+ }
+ if (typeof WallpaperService !== "undefined" && WallpaperService.refreshWallpapersList) {
+ WallpaperService.refreshWallpapersList()
+ }
+ Qt.callLater(refreshWallpapers)
+ }
+
+ Connections {
+ target: WallpaperService
+ enabled: typeof WallpaperService !== "undefined"
+ function onWallpaperListChanged(screenName, count) {
+ if (screenName === Screen.name) {
+ Qt.callLater(refreshWallpapers)
+ }
+ }
+ }
+
+ Timer {
+ id: initialRefreshTimer
+ interval: 1000
+ running: false
+ repeat: false
+ onTriggered: refreshWallpapers()
+ }
+
+ Component.onCompleted: {
+ if (typeof Settings !== "undefined" && Settings.data && Settings.data.wallpaper && Settings.data.wallpaper.directory) {
+ selectedDirectory = Settings.data.wallpaper.directory
+ } else {
+ selectedDirectory = Quickshell.env("HOME") + "/Pictures/Wallpapers"
+ }
+ if (typeof WallpaperService !== "undefined" && WallpaperService.currentWallpaper) {
+ selectedWallpaper = WallpaperService.currentWallpaper
+ }
+ initialRefreshTimer.start()
+ }
+
+ NFilePicker {
+ id: directoryPicker
+ selectionMode: "folders"
+ title: I18n.tr("setup.wallpaper.dir.select-title")
+ initialPath: selectedDirectory || Quickshell.env("HOME") + "/Pictures"
+ onAccepted: paths => {
+ if (paths.length > 0) {
+ selectedDirectory = paths[0]
+ directoryChanged(paths[0])
+ }
+ }
+ }
+
+ Process {
+ id: directoryScanner
+ command: ["find", "", "-type", "f", "\\(-iname", "*.jpg", "-o", "-iname", "*.jpeg", "-o", "-iname", "*.png", "-o", "-iname", "*.bmp", "-o", "-iname", "*.webp", "-o", "-iname", "*.svg", "\\)"]
+ running: false
+ stdout: StdioCollector {}
+ stderr: StdioCollector {}
+ onExited: function (exitCode) {
+ if (exitCode === 0) {
+ var lines = stdout.text.split('\n')
+ var images = []
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i].trim()
+ if (line !== '') {
+ images.push(line)
+ }
+ }
+ wallpapersList = images
+ updateFilteredWallpapers()
+ if (wallpapersList.length > 0 && selectedWallpaper === "") {
+ selectedWallpaper = wallpapersList[0]
+ }
+ }
+ }
+ }
+}
diff --git a/Modules/SetupWizard/SetupWizard.qml b/Modules/SetupWizard/SetupWizard.qml
new file mode 100644
index 00000000..d8f79bb7
--- /dev/null
+++ b/Modules/SetupWizard/SetupWizard.qml
@@ -0,0 +1,414 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Wayland
+import qs.Commons
+import qs.Services
+import qs.Widgets
+
+NPanel {
+ id: root
+
+ preferredWidth: 520
+ preferredHeight: 600
+ preferredWidthRatio: 0.4
+ preferredHeightRatio: 0.6
+ panelAnchorHorizontalCenter: true
+ panelAnchorVerticalCenter: true
+ panelKeyboardFocus: true
+
+ // Prevent closing during setup
+ backgroundClickEnabled: false
+ draggable: false
+
+ property int currentStep: 0
+ property int totalSteps: 4
+
+ // Setup wizard data
+ property string selectedWallpaperDirectory: Settings.defaultWallpapersDirectory
+ property string selectedWallpaper: ""
+ property real selectedScaleRatio: 1.0
+ property string selectedBarPosition: "top"
+ property bool selectedDimDesktop: true
+
+ panelContent: Component {
+ Item {
+ id: container
+ anchors.fill: parent
+
+ ColumnLayout {
+ id: wizardContent
+ anchors.fill: parent
+ anchors.margins: Style.marginXL
+ spacing: Style.marginL
+
+ // Override ESC key to prevent closing during setup
+ Shortcut {
+ sequences: ["Escape"]
+ enabled: root.active
+ onActivated: {
+
+ // Do nothing - prevent ESC from closing the setup wizard
+ }
+ context: Qt.WindowShortcut
+ }
+
+ // Step content - takes most of the space
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.minimumHeight: 300
+
+ StackLayout {
+ id: stepStack
+ anchors.fill: parent
+ currentIndex: currentStep
+
+ // Step 0: Welcome - Beautiful centered design
+ Item {
+ ColumnLayout {
+ anchors.centerIn: parent
+ width: Math.min(parent.width - Style.marginXL * 2, 420)
+ spacing: Style.marginXL
+
+ // Logo with subtle glow effect
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 120
+ Layout.alignment: Qt.AlignHCenter
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: 120
+ height: 120
+ radius: width / 2
+ color: Color.mPrimary
+ opacity: 0.08
+ scale: 1.3
+ }
+
+ Image {
+ anchors.centerIn: parent
+ width: 110
+ height: 110
+ source: Qt.resolvedUrl(Quickshell.shellDir + "/Assets/noctalia.svg")
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mSurfaceVariant
+ radius: width / 2
+ border.color: Color.mOutline
+ border.width: 2
+ visible: parent.status === Image.Error
+
+ NIcon {
+ icon: "sparkles"
+ pointSize: Style.fontSizeXXL * 1.5
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ // Subtle pulse animation
+ SequentialAnimation on scale {
+ running: true
+ loops: Animation.Infinite
+ NumberAnimation {
+ from: 1.0
+ to: 1.05
+ duration: 2000
+ easing.type: Easing.InOutQuad
+ }
+ NumberAnimation {
+ from: 1.05
+ to: 1.0
+ duration: 2000
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+ }
+
+ // Welcome text with gradient feel
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter
+ spacing: Style.marginM
+
+ NText {
+ text: "Welcome to Noctalia! ✨"
+ pointSize: Style.fontSizeXXL * 1.4
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ NText {
+ text: "Let's make your desktop uniquely yours"
+ pointSize: Style.fontSizeL
+ color: Color.mOnSurfaceVariant
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ }
+
+ // Friendly subtext
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.topMargin: Style.marginL
+ Layout.preferredHeight: childrenRect.height + Style.marginM * 2
+ color: Color.mSurfaceVariant
+ radius: Style.radiusL
+ opacity: 0.4
+
+ NText {
+ anchors.centerIn: parent
+ width: parent.width - Style.marginL * 2
+ text: I18n.tr("setup.welcome.note")
+ pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ }
+ }
+ }
+ }
+ }
+
+ // Step 1: Wallpaper Setup
+ SetupWallpaperStep {
+ id: step1
+ selectedDirectory: root.selectedWallpaperDirectory
+ selectedWallpaper: root.selectedWallpaper
+ onDirectoryChanged: function (directory) {
+ root.selectedWallpaperDirectory = directory
+ root.applyWallpaperSettings()
+ }
+ onWallpaperChanged: function (wallpaper) {
+ root.selectedWallpaper = wallpaper
+ root.applyWallpaperSettings()
+ }
+ }
+
+ // Step 2: UI Configuration
+ SetupCustomizeStep {
+ id: step2
+ selectedScaleRatio: root.selectedScaleRatio
+ selectedBarPosition: root.selectedBarPosition
+ selectedDimDesktop: root.selectedDimDesktop
+ onScaleRatioChanged: function (ratio) {
+ root.selectedScaleRatio = ratio
+ root.applyUISettings()
+ }
+ onBarPositionChanged: function (position) {
+ root.selectedBarPosition = position
+ root.applyUISettings()
+ }
+ onDimDesktopChanged: function (dim) {
+ root.selectedDimDesktop = dim
+ root.applyUISettings()
+ }
+ }
+
+ // Step 3: Appearance - Dark mode and color source
+ SetupAppearanceStep {
+ id: step3
+ }
+ }
+ }
+
+ // Elegant divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ }
+
+ // Modern progress indicator with labels
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 32
+
+ RowLayout {
+ anchors.centerIn: parent
+ spacing: Style.marginM
+
+ Repeater {
+ model: [{
+ "icon": "sparkles",
+ "label": "Welcome"
+ }, {
+ "icon": "image",
+ "label": "Wallpaper"
+ }, {
+ "icon": "settings",
+ "label": "Customize"
+ }, {
+ "icon": "palette",
+ "label": "Appearance"
+ }]
+ delegate: RowLayout {
+ spacing: Style.marginS
+
+ Rectangle {
+ width: 24
+ height: 24
+ radius: width / 2
+ color: index <= currentStep ? Color.mPrimary : Color.mSurfaceVariant
+ border.color: index === currentStep ? Color.mPrimary : "transparent"
+ border.width: index === currentStep ? 2 : 0
+
+ NIcon {
+ icon: modelData.icon
+ pointSize: Style.fontSizeS
+ color: index <= currentStep ? Color.mOnPrimary : Color.mOnSurfaceVariant
+ anchors.centerIn: parent
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationNormal
+ }
+ }
+ }
+
+ NText {
+ text: modelData.label
+ pointSize: Style.fontSizeS
+ color: index <= currentStep ? Color.mPrimary : Color.mOnSurfaceVariant
+ font.weight: index === currentStep ? Style.fontWeightBold : Style.fontWeightRegular
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationNormal
+ }
+ }
+ }
+
+ // Connector line
+ Rectangle {
+ width: 40
+ height: 2
+ radius: 1
+ color: index < currentStep ? Color.mPrimary : Color.mSurfaceVariant
+ visible: index < totalSteps - 1
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationNormal
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Smooth navigation buttons
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 44
+ Layout.topMargin: Style.marginS
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: Style.marginM
+
+ NButton {
+ text: "Skip Setup"
+ outlined: true
+ visible: currentStep === 0
+ Layout.preferredHeight: 44
+ onClicked: {
+ root.completeSetup()
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ NButton {
+ text: "← Back"
+ outlined: true
+ visible: currentStep > 0
+ Layout.preferredHeight: 44
+ onClicked: {
+ if (currentStep > 0) {
+ currentStep--
+ }
+ }
+ }
+
+ NButton {
+ text: currentStep === totalSteps - 1 ? "All Done!" : "Continue →"
+ Layout.preferredHeight: 44
+ onClicked: {
+ if (currentStep < totalSteps - 1) {
+ currentStep++
+ } else {
+ root.completeSetup()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ function completeSetup() {
+ Logger.i("SetupWizard", "Completing setup with selected options")
+
+ if (selectedWallpaperDirectory !== Settings.data.wallpaper.directory) {
+ Settings.data.wallpaper.directory = selectedWallpaperDirectory
+ WallpaperService.refreshWallpapersList()
+ }
+
+ if (selectedWallpaper !== "") {
+ WallpaperService.changeWallpaper(selectedWallpaper, undefined)
+ }
+
+ Settings.data.general.scaleRatio = selectedScaleRatio
+ Settings.data.bar.position = selectedBarPosition
+ Settings.data.general.dimDesktop = selectedDimDesktop
+ Settings.data.setupCompleted = true
+
+ Settings.saveImmediate()
+ Logger.i("SetupWizard", "Setup completed successfully")
+ root.close()
+ }
+
+ function applyWallpaperSettings() {
+ if (selectedWallpaperDirectory !== Settings.data.wallpaper.directory) {
+ Settings.data.wallpaper.directory = selectedWallpaperDirectory
+ WallpaperService.refreshWallpapersList()
+ }
+
+ if (selectedWallpaper !== "") {
+ WallpaperService.changeWallpaper(selectedWallpaper, undefined)
+ }
+ }
+
+ function applyUISettings() {
+ Settings.data.general.scaleRatio = selectedScaleRatio
+ Settings.data.bar.position = selectedBarPosition
+ Settings.data.general.dimDesktop = selectedDimDesktop
+ }
+
+ Component.onCompleted: {
+ Logger.i("SetupWizard", "Setup wizard opened")
+ // Initialize selections from existing settings to avoid overwriting user config
+ if (Settings && Settings.data) {
+ selectedScaleRatio = Settings.data.general.scaleRatio
+ selectedBarPosition = Settings.data.bar.position
+ selectedDimDesktop = Settings.data.general.dimDesktop
+ selectedWallpaperDirectory = Settings.data.wallpaper.directory || Settings.defaultWallpapersDirectory
+ }
+ }
+}
diff --git a/Modules/Toast/ToastScreen.qml b/Modules/Toast/ToastScreen.qml
index ee19a625..badb00b6 100644
--- a/Modules/Toast/ToastScreen.qml
+++ b/Modules/Toast/ToastScreen.qml
@@ -44,11 +44,11 @@ Item {
function enqueueToast(toastData) {
// Safe logging - fix the substring bug
var descPreview = (toastData.description || "").substring(0, 100).replace(/\n/g, " ")
- Logger.log("ToastScreen", "Queuing", toastData.type, ":", toastData.message, descPreview)
+ Logger.i("ToastScreen", "Queuing", toastData.type, ":", toastData.message, descPreview)
// Bounded queue to prevent unbounded memory growth
if (messageQueue.length >= maxQueueSize) {
- Logger.log("ToastScreen", "Queue full, dropping oldest toast")
+ Logger.i("ToastScreen", "Queue full, dropping oldest toast")
messageQueue.shift()
}
diff --git a/Modules/Tooltip/Tooltip.qml b/Modules/Tooltip/Tooltip.qml
index 43a6850c..00c139b6 100644
--- a/Modules/Tooltip/Tooltip.qml
+++ b/Modules/Tooltip/Tooltip.qml
@@ -321,11 +321,36 @@ PopupWindow {
completeHide()
}
- // Update text function for binding support
+ // Update text function
function updateText(newText) {
if (visible && targetItem) {
text = newText
- positionAndShow()
+
+ // Recalculate dimensions
+ const tipWidth = Math.min(tooltipText.implicitWidth + (padding * 2), maxWidth)
+ root.implicitWidth = tipWidth
+
+ const tipHeight = tooltipText.implicitHeight + (padding * 2)
+ root.implicitHeight = tipHeight
+
+ // Reposition if necessary
+ var targetGlobal = targetItem.mapToItem(null, 0, 0)
+ const targetWidth = targetItem.width
+
+ // Adjust horizontal position to keep tooltip on screen if needed
+ const globalX = targetGlobal.x + anchorX
+ if (globalX < 0) {
+ anchorX = -targetGlobal.x + margin
+ } else if (globalX + tipWidth > screenWidth) {
+ anchorX = screenWidth - targetGlobal.x - tipWidth - margin
+ }
+
+ // Force anchor update
+ Qt.callLater(() => {
+ if (root.anchor && root.visible) {
+ root.anchor.updateAnchor()
+ }
+ })
}
}
diff --git a/Modules/Wallpaper/WallpaperPanel.qml b/Modules/Wallpaper/WallpaperPanel.qml
index be3a526b..2dcfe18e 100644
--- a/Modules/Wallpaper/WallpaperPanel.qml
+++ b/Modules/Wallpaper/WallpaperPanel.qml
@@ -423,7 +423,6 @@ NPanel {
NIcon {
icon: "check"
pointSize: Style.fontSizeM
- font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
diff --git a/README.md b/README.md
index 8c03cb67..a577af03 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,7 @@ While all donations are greatly appreciated, they are completely voluntary.
* Gohma
* DiscoCevapi
* PikaOS
+* LionHeartP
---
diff --git a/Services/AppThemeService.qml b/Services/AppThemeService.qml
index 53f7fe75..49d7bd60 100644
--- a/Services/AppThemeService.qml
+++ b/Services/AppThemeService.qml
@@ -81,14 +81,14 @@ Singleton {
Connections {
target: Settings.data.colorSchemes
function onDarkModeChanged() {
- Logger.log("AppThemeService", "Detected dark mode change")
+ Logger.i("AppThemeService", "Detected dark mode change")
AppThemeService.generate()
}
}
// --------------------------------------------------------------------------------
function init() {
- Logger.log("AppThemeService", "Service started")
+ Logger.i("AppThemeService", "Service started")
}
// --------------------------------------------------------------------------------
@@ -105,11 +105,11 @@ Singleton {
// Wallpaper Colors Generation
// --------------------------------------------------------------------------------
function generateFromWallpaper() {
- Logger.log("AppThemeService", "Generating from wallpaper on screen:", Screen.name)
+ // Logger.i("AppThemeService", "Generating from wallpaper on screen:", Screen.name)
const wp = WallpaperService.getWallpaper(Screen.name).replace(/'/g, "'\\''")
if (!wp) {
- Logger.error("AppThemeService", "No wallpaper found")
+ Logger.e("AppThemeService", "No wallpaper found")
return
}
@@ -141,7 +141,7 @@ Singleton {
// Instead, we use 'sed' to apply a custom palette to the existing matugen templates.
// --------------------------------------------------------------------------------
function generateFromPredefinedScheme(schemeData) {
- Logger.log("AppThemeService", "Generating templates from predefined color scheme")
+ Logger.i("AppThemeService", "Generating templates from predefined color scheme")
handleTerminalThemes()
@@ -150,7 +150,10 @@ Singleton {
const colors = schemeData[mode]
const matugenColors = generatePalette(colors.mPrimary, colors.mSecondary, colors.mTertiary, colors.mError, colors.mSurface, isDarkMode)
- const script = processAllTemplates(matugenColors, mode)
+ let script = processAllTemplates(matugenColors, mode)
+
+ // Add user templates if enabled
+ script += buildUserTemplateCommandForPredefined(schemeData, mode)
generateProcess.command = ["bash", "-lc", script]
generateProcess.running = true
@@ -159,7 +162,8 @@ Singleton {
function generatePalette(primaryColor, secondaryColor, tertiaryColor, errorColor, backgroundColor, outlineColor, isDarkMode) {
const c = hex => ({
"default": {
- "hex": hex
+ "hex": hex,
+ "hex_stripped": hex.replace(/^#/, "")
}
})
@@ -335,8 +339,42 @@ Singleton {
return script
}
+ function buildUserTemplateCommandForPredefined(schemeData, mode) {
+ if (!Settings.data.templates.enableUserTemplates) {
+ return ""
+ }
+
+ const userConfigPath = getUserConfigPath()
+ const isDarkMode = Settings.data.colorSchemes.darkMode
+ const colors = schemeData[mode]
+
+ // Generate the matugen palette JSON
+ const matugenColors = generatePalette(colors.mPrimary, colors.mSecondary, colors.mTertiary, colors.mError, colors.mSurface, isDarkMode)
+
+ // Create a temporary JSON file with the color palette
+ const tempJsonPath = Settings.cacheDir + "predefined-colors.json"
+ const homeDir = Quickshell.env("HOME")
+ const tempJsonPathEsc = tempJsonPath.replace(/'/g, "'\\''")
+
+ let script = "\n# Execute user templates with predefined scheme colors\n"
+ script += `if [ -f '${userConfigPath}' ]; then\n`
+
+ // Write the color palette to a temp JSON file
+ script += ` cat > '${tempJsonPathEsc}' << 'EOF'\n`
+ script += JSON.stringify({
+ "colors": matugenColors
+ }, null, 2) + "\n"
+ script += "EOF\n"
+
+ // Use matugen json subcommand with the color palette
+ script += ` matugen json '${tempJsonPathEsc}' --config '${userConfigPath}' --mode ${mode}\n`
+ script += "fi"
+
+ return script
+ }
+
function getUserConfigPath() {
- return (Quickshell.env("HOME") + "/.config/matugen/config.toml").replace(/'/g, "'\\''")
+ return (Settings.configDir + "user-templates.toml").replace(/'/g, "'\\''")
}
// --------------------------------------------------------------------------------
@@ -346,10 +384,17 @@ Singleton {
id: generateProcess
workingDirectory: Quickshell.shellDir
running: false
+ stdout: StdioCollector {
+ onStreamFinished: {
+ if (this.text) {
+ Logger.d("AppThemeService", "GenerateProcess stdout:", this.text)
+ }
+ }
+ }
stderr: StdioCollector {
onStreamFinished: {
if (this.text) {
- Logger.warn("AppThemeService", "GenerateProcess stderr:", this.text)
+ Logger.d("AppThemeService", "GenerateProcess stderr:", this.text)
}
}
}
@@ -357,11 +402,12 @@ Singleton {
Process {
id: copyProcess
+ workingDirectory: Quickshell.shellDir
running: false
stderr: StdioCollector {
onStreamFinished: {
if (this.text) {
- Logger.warn("AppThemeService", "CopyProcess stderr:", this.text)
+ Logger.d("AppThemeService", "CopyProcess stderr:", this.text)
}
}
}
diff --git a/Services/AudioService.qml b/Services/AudioService.qml
index 739fd221..38182d8a 100644
--- a/Services/AudioService.qml
+++ b/Services/AudioService.qml
@@ -61,7 +61,7 @@ Singleton {
function onMutedChanged() {
root._muted = (sink?.audio.muted ?? true)
- Logger.log("AudioService", "OnMuteChanged:", root._muted)
+ Logger.i("AudioService", "OnMuteChanged:", root._muted)
ToastService.showNotice(I18n.tr("settings.audio.devices.output-device.label"), root._muted ? I18n.tr("toast.audio.muted") : I18n.tr("toast.audio.unmuted"))
}
}
@@ -79,7 +79,7 @@ Singleton {
function onMutedChanged() {
root._inputMuted = (source?.audio.muted ?? true)
- Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted)
+ Logger.i("AudioService", "OnInputMuteChanged:", root._inputMuted)
ToastService.showNotice(I18n.tr("settings.audio.devices.input-device.label"), root._inputMuted ? I18n.tr("toast.audio.muted") : I18n.tr("toast.audio.unmuted"))
}
}
@@ -97,9 +97,9 @@ Singleton {
// Clamp it accordingly
sink.audio.muted = false
sink.audio.volume = Math.max(0, Math.min(Settings.data.audio.volumeOverdrive ? 1.5 : 1.0, newVolume))
- //Logger.log("AudioService", "SetVolume", sink.audio.volume);
+ //Logger.i("AudioService", "SetVolume", sink.audio.volume);
} else {
- Logger.warn("AudioService", "No sink available")
+ Logger.w("AudioService", "No sink available")
}
}
@@ -107,7 +107,7 @@ Singleton {
if (sink?.ready && sink?.audio) {
sink.audio.muted = muted
} else {
- Logger.warn("AudioService", "No sink available")
+ Logger.w("AudioService", "No sink available")
}
}
@@ -125,7 +125,7 @@ Singleton {
source.audio.muted = false
source.audio.volume = Math.max(0, Math.min(Settings.data.audio.volumeOverdrive ? 1.5 : 1.0, newVolume))
} else {
- Logger.warn("AudioService", "No source available")
+ Logger.w("AudioService", "No source available")
}
}
@@ -133,7 +133,7 @@ Singleton {
if (source?.ready && source?.audio) {
source.audio.muted = muted
} else {
- Logger.warn("AudioService", "No source available")
+ Logger.w("AudioService", "No source available")
}
}
diff --git a/Services/BarService.qml b/Services/BarService.qml
index 0a839294..db584218 100644
--- a/Services/BarService.qml
+++ b/Services/BarService.qml
@@ -38,14 +38,14 @@ Singleton {
}
Component.onCompleted: {
- Logger.log("BarService", "Service started")
+ Logger.d("BarService", "Service started")
}
// Function for the Bar to call when it's ready
function registerBar(screenName) {
if (!readyBars[screenName]) {
readyBars[screenName] = true
- Logger.log("BarService", "Bar is ready on screen:", screenName)
+ Logger.d("BarService", "Bar is ready on screen:", screenName)
barReadyChanged(screenName)
}
}
@@ -69,7 +69,7 @@ Singleton {
timerCheckVisualizer.restart()
- Logger.log("BarService", "Registered widget:", key)
+ Logger.d("BarService", "Registered widget:", key)
root.activeWidgetsChanged()
}
@@ -77,7 +77,7 @@ Singleton {
function unregisterWidget(screenName, section, widgetId, index) {
const key = [screenName, section, widgetId, index].join("|")
delete widgetInstances[key]
- Logger.log("BarService", "Unregistered widget:", key)
+ Logger.d("BarService", "Unregistered widget:", key)
root.activeWidgetsChanged()
}
@@ -219,7 +219,7 @@ Singleton {
}
}
} catch (e) {
- Logger.error(e)
+ Logger.e(e)
}
return false
}
diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml
index 1ce6affc..8535eba1 100644
--- a/Services/BarWidgetRegistry.qml
+++ b/Services/BarWidgetRegistry.qml
@@ -45,7 +45,8 @@ Singleton {
"hideMode": "hidden",
"scrollingMode"// "visible", "hidden", "transparent"
: "hover",
- "width": 145,
+ "maxWidth": 145,
+ "useFixedWidth": false,
"colorizeIcons": false
},
"Battery": {
@@ -89,6 +90,8 @@ Singleton {
"hideMode": "hidden",
"scrollingMode"// "visible", "hidden", "transparent"
: "hover",
+ "maxWidth": 145,
+ "useFixedWidth": false,
"showAlbumArt": false,
"showVisualizer": false,
"visualizerType": "linear"
@@ -222,7 +225,7 @@ Singleton {
}
function init() {
- Logger.log("BarWidgetRegistry", "Service started")
+ Logger.i("BarWidgetRegistry", "Service started")
}
// ------------------------------
diff --git a/Services/BatteryService.qml b/Services/BatteryService.qml
index cf64b853..4977233c 100644
--- a/Services/BatteryService.qml
+++ b/Services/BatteryService.qml
@@ -60,7 +60,6 @@ Singleton {
if (enabled) {
setChargingMode(BatteryService.ChargingMode.Full)
} else {
- BatteryService.chargingMode = BatteryService.ChargingMode.Disabled
BatteryService.initialSetter = true
ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-setup"))
PanelService.getPanel("batteryPanel")?.toggle(this)
@@ -70,7 +69,7 @@ Singleton {
function setChargingMode(newMode) {
if (newMode !== BatteryService.ChargingMode.Full && newMode !== BatteryService.ChargingMode.Balanced && newMode !== BatteryService.ChargingMode.Lifespan) {
- Logger.warn("BatteryService", `Invalid charging mode set ${newMode}`)
+ Logger.w("BatteryService", `Invalid charging mode set ${newMode}`)
return
}
BatteryService.chargingMode = newMode
@@ -113,7 +112,7 @@ Singleton {
running: false
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
- Logger.log("BatteryService", "Battery threshold set successfully")
+ Logger.i("BatteryService", "Battery threshold set successfully")
if (BatteryService.initialSetter) {
BatteryService.initialSetter = false
return
@@ -128,20 +127,20 @@ Singleton {
BatteryService.runInstaller()
} else {
ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.set-failed"))
- Logger.error("BatteryService", `Setter process failed with exit code: ${exitCode}`)
+ Logger.e("BatteryService", `Setter process failed with exit code: ${exitCode}`)
}
}
stderr: StdioCollector {
onStreamFinished: {
if (this.text) {
- Logger.warn("BatteryService", "SetterProcess stderr:", this.text)
+ Logger.w("BatteryService", "SetterProcess stderr:", this.text)
}
}
}
stdout: StdioCollector {
onStreamFinished: {
if (this.text) {
- Logger.log("BatteryService", "SetterProcess stdout:", this.text)
+ Logger.i("BatteryService", "SetterProcess stdout:", this.text)
}
}
}
@@ -171,14 +170,14 @@ Singleton {
stderr: StdioCollector {
onStreamFinished: {
if (this.text) {
- Logger.warn("BatteryService", "InstallerProcess stderr:", this.text)
+ Logger.w("BatteryService", "InstallerProcess stderr:", this.text)
}
}
}
stdout: StdioCollector {
onStreamFinished: {
if (this.text) {
- Logger.log("BatteryService", "InstallerProcess stdout:", this.text)
+ Logger.i("BatteryService", "InstallerProcess stdout:", this.text)
}
}
}
@@ -191,26 +190,27 @@ Singleton {
running: false
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
- Logger.log("BatteryService", "Battery Manager uninstalled successfully")
+ Logger.i("BatteryService", "Battery Manager uninstalled successfully")
ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-success"))
Settings.data.battery.chargingMode = BatteryService.chargingMode
+ BatteryService.chargingMode = BatteryService.ChargingMode.Disabled
cleanupProcess.running = true
} else {
ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-failed"))
- Logger.error("BatteryService", `Uninstaller process failed with exit code: ${exitCode}`)
+ Logger.e("BatteryService", `Uninstaller process failed with exit code: ${exitCode}`)
}
}
stderr: StdioCollector {
onStreamFinished: {
if (this.text) {
- Logger.warn("BatteryService", "UninstallerProcess stderr:", this.text)
+ Logger.w("BatteryService", "UninstallerProcess stderr:", this.text)
}
}
}
stdout: StdioCollector {
onStreamFinished: {
if (this.text) {
- Logger.log("BatteryService", "UninstallerProcess stdout:", this.text)
+ Logger.i("BatteryService", "UninstallerProcess stdout:", this.text)
}
}
}
@@ -224,22 +224,22 @@ Singleton {
running: false
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
- Logger.log("BatteryService", "Battery Manager uninstalled successfully")
+ Logger.i("BatteryService", "Battery Manager uninstalled successfully")
} else {
- Logger.error("BatteryService", `Cleanup process failed with exit code: ${exitCode}`)
+ Logger.e("BatteryService", `Cleanup process failed with exit code: ${exitCode}`)
}
}
stderr: StdioCollector {
onStreamFinished: {
if (this.text) {
- Logger.warn("BatteryService", "CleanupProcess stderr:", this.text)
+ Logger.w("BatteryService", "CleanupProcess stderr:", this.text)
}
}
}
stdout: StdioCollector {
onStreamFinished: {
if (this.text) {
- Logger.log("BatteryService", "CleanupProcess stdout:", this.text)
+ Logger.i("BatteryService", "CleanupProcess stdout:", this.text)
}
}
}
diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml
index c5e17d62..d6c56e79 100644
--- a/Services/BluetoothService.qml
+++ b/Services/BluetoothService.qml
@@ -32,7 +32,7 @@ Singleton {
}
function init() {
- Logger.log("Bluetooth", "Service initialized")
+ Logger.i("Bluetooth", "Service initialized")
}
Timer {
@@ -46,11 +46,11 @@ Singleton {
target: adapter
function onEnabledChanged() {
if (!adapter) {
- Logger.warn("Bluetooth", "onEnabledChanged", "No adapter available")
+ Logger.w("Bluetooth", "onEnabledChanged", "No adapter available")
return
}
- Logger.log("Bluetooth", "onEnableChanged", adapter.enabled)
+ Logger.d("Bluetooth", "onEnableChanged", adapter.enabled)
if (adapter.enabled) {
ToastService.showNotice(I18n.tr("bluetooth.panel.title"), I18n.tr("toast.bluetooth.enabled"))
discoveryTimer.running = true
@@ -171,22 +171,22 @@ Singleton {
function getSignalIcon(device) {
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
- return "signal_cellular_null"
+ return "antenna-bars-off"
}
var signal = device.signalStrength
if (signal >= 80) {
- return "signal_cellular_4_bar"
+ return "antenna-bars-5"
}
if (signal >= 60) {
- return "signal_cellular_3_bar"
+ return "antenna-bars-4"
}
if (signal >= 40) {
- return "signal_cellular_2_bar"
+ return "antenna-bars-3"
}
if (signal >= 20) {
- return "signal_cellular_1_bar"
+ return "antenna-bars-2"
}
- return "signal_cellular_0_bar"
+ return "antenna-bars-1"
}
function isDeviceBusy(device) {
@@ -225,11 +225,11 @@ Singleton {
function setBluetoothEnabled(state) {
if (!adapter) {
- Logger.warn("Bluetooth", "No adapter available")
+ Logger.w("Bluetooth", "No adapter available")
return
}
- Logger.log("Bluetooth", "SetBluetoothEnabled", state)
+ Logger.i("Bluetooth", "SetBluetoothEnabled", state)
adapter.enabled = state
}
}
diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml
index e4243640..f98d9215 100644
--- a/Services/BrightnessService.qml
+++ b/Services/BrightnessService.qml
@@ -46,7 +46,7 @@ Singleton {
reloadableId: "brightness"
Component.onCompleted: {
- Logger.log("Brightness", "Service started")
+ Logger.i("Brightness", "Service started")
}
onMonitorsChanged: {
@@ -85,7 +85,7 @@ Singleton {
var ddcModel = ddcModelMatch ? ddcModelMatch.length > 0 : false
var model = modelMatch ? modelMatch[1] : "Unknown"
var bus = busMatch ? busMatch[1] : "Unknown"
- Logger.log("Brigthness", "Detected DDC Monitor:", model, "on bus", bus, "is DDC:", !ddcModel)
+ Logger.i("Brigthness", "Detected DDC Monitor:", model, "on bus", bus, "is DDC:", !ddcModel)
return {
"model": model,
"busNum": bus,
@@ -141,7 +141,7 @@ Singleton {
monitor.brightness = newBrightness
monitor.brightnessUpdated(monitor.brightness)
root.monitorBrightnessChanged(monitor, monitor.brightness)
- //Logger.log("Brightness", "Refreshed brightness from system:", monitor.modelData.name, monitor.brightness)
+ //Logger.i("Brightness", "Refreshed brightness from system:", monitor.modelData.name, monitor.brightness)
}
}
}
@@ -190,12 +190,12 @@ Singleton {
return
}
- //Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
+ //Logger.i("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
if (monitor.isAppleDisplay) {
var val = parseInt(dataText)
if (!isNaN(val)) {
monitor.brightness = val / 101
- Logger.log("Brightness", "Apple display brightness:", monitor.brightness)
+ Logger.d("Brightness", "Apple display brightness:", monitor.brightness)
}
} else if (monitor.isDdc) {
var parts = dataText.split(" ")
@@ -204,7 +204,7 @@ Singleton {
var max = parseInt(parts[4])
if (!isNaN(current) && !isNaN(max) && max > 0) {
monitor.brightness = current / max
- Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness)
+ Logger.d("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness)
}
}
} else {
@@ -220,8 +220,8 @@ Singleton {
if (!isNaN(current) && !isNaN(max) && max > 0) {
monitor.maxBrightness = max
monitor.brightness = current / max
- Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
- Logger.log("Brightness", "Using backlight device:", monitor.backlightDevice)
+ Logger.d("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
+ Logger.d("Brightness", "Using backlight device:", monitor.backlightDevice)
}
}
}
diff --git a/Services/CavaService.qml b/Services/CavaService.qml
index 07b8de7f..527f0275 100644
--- a/Services/CavaService.qml
+++ b/Services/CavaService.qml
@@ -40,7 +40,7 @@ Singleton {
running: root.shouldRun
command: ["cava", "-p", "/dev/stdin"]
onRunningChanged: {
- Logger.log("Cava", "Process running:", running)
+ Logger.d("Cava", "Process running:", running)
}
onExited: {
stdinEnabled = true
diff --git a/Services/ColorSchemeService.qml b/Services/ColorSchemeService.qml
index 7e07f27a..4d78df2a 100644
--- a/Services/ColorSchemeService.qml
+++ b/Services/ColorSchemeService.qml
@@ -18,7 +18,7 @@ Singleton {
Connections {
target: Settings.data.colorSchemes
function onDarkModeChanged() {
- Logger.log("ColorScheme", "Detected dark mode change")
+ Logger.i("ColorScheme", "Detected dark mode change")
if (!Settings.data.colorSchemes.useWallpaperColors && Settings.data.colorSchemes.predefinedScheme) {
// Re-apply current scheme to pick the right variant
applyScheme(Settings.data.colorSchemes.predefinedScheme)
@@ -35,12 +35,12 @@ Singleton {
function init() {
// does nothing but ensure the singleton is created
// do not remove
- Logger.log("ColorScheme", "Service started")
+ Logger.i("ColorScheme", "Service started")
loadColorSchemes()
}
function loadColorSchemes() {
- Logger.log("ColorScheme", "Load colorScheme")
+ Logger.d("ColorScheme", "Load colorScheme")
scanning = true
schemes = []
// Use find command to locate all scheme.json files
@@ -108,7 +108,7 @@ Singleton {
})
schemes = files
scanning = false
- Logger.log("ColorScheme", "Listed", schemes.length, "schemes")
+ Logger.d("ColorScheme", "Listed", schemes.length, "schemes")
// Normalize stored scheme to basename and re-apply if necessary
var stored = Settings.data.colorSchemes.predefinedScheme
if (stored) {
@@ -121,7 +121,7 @@ Singleton {
}
}
} else {
- Logger.error("ColorScheme", "Failed to find color scheme files")
+ Logger.e("ColorScheme", "Failed to find color scheme files")
schemes = []
scanning = false
}
@@ -147,14 +147,14 @@ Singleton {
}
}
writeColorsToDisk(variant)
- Logger.log("ColorScheme", "Applying color scheme:", getBasename(path))
+ Logger.i("ColorScheme", "Applying color scheme:", getBasename(path))
// Generate Matugen templates if any are enabled and setting allows it
if (Settings.data.colorSchemes.generateTemplatesForPredefined && hasEnabledTemplates()) {
AppThemeService.generateFromPredefinedScheme(data)
}
} catch (e) {
- Logger.error("ColorScheme", "Failed to parse scheme JSON:", path, e)
+ Logger.e("ColorScheme", "Failed to parse scheme JSON:", path, e)
}
}
}
@@ -171,7 +171,7 @@ Singleton {
path: colorsJsonFilePath
onSaved: {
- // Logger.log("ColorScheme", "Colors saved")
+ // Logger.i("ColorScheme", "Colors saved")
}
JsonAdapter {
id: out
diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml
index 5640c9d4..307dc9b7 100644
--- a/Services/CompositorService.qml
+++ b/Services/CompositorService.qml
@@ -100,13 +100,13 @@ Singleton {
// Load cached display scales
displayScales = displayCacheAdapter.displays || {}
displayScalesLoaded = true
- // Logger.log("CompositorService", "Loaded display scales from cache:", JSON.stringify(displayScales))
+ // 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.log("CompositorService", "No display cache found, will create on first update")
+ // Logger.i("CompositorService", "No display cache found, will create on first update")
}
}
@@ -195,7 +195,7 @@ Singleton {
// Update display scales from backend
function updateDisplayScales() {
if (!backend || !backend.queryDisplayScales) {
- Logger.warn("CompositorService", "Backend does not support display scale queries")
+ Logger.w("CompositorService", "Backend does not support display scale queries")
return
}
@@ -207,7 +207,7 @@ Singleton {
displayScales = scales
saveDisplayScalesToCache()
displayScalesChanged()
- Logger.log("CompositorService", "Display scales updated")
+ Logger.i("CompositorService", "Display scales updated")
}
// Save display scales to cache
@@ -261,7 +261,7 @@ Singleton {
if (backend && backend.switchToWorkspace) {
backend.switchToWorkspace(workspace)
} else {
- Logger.warn("Compositor", "No backend available for workspace switching")
+ Logger.w("Compositor", "No backend available for workspace switching")
}
}
@@ -293,7 +293,7 @@ Singleton {
if (backend && backend.focusWindow) {
backend.focusWindow(window)
} else {
- Logger.warn("Compositor", "No backend available for window focus")
+ Logger.w("Compositor", "No backend available for window focus")
}
}
@@ -302,43 +302,43 @@ Singleton {
if (backend && backend.closeWindow) {
backend.closeWindow(window)
} else {
- Logger.warn("Compositor", "No backend available for window closing")
+ Logger.w("Compositor", "No backend available for window closing")
}
}
// Session management
function logout() {
if (backend && backend.logout) {
- Logger.log("Compositor", "Logout requested")
+ Logger.i("Compositor", "Logout requested")
backend.logout()
} else {
- Logger.warn("Compositor", "No backend available for logout")
+ Logger.w("Compositor", "No backend available for logout")
}
}
function shutdown() {
- Logger.log("Compositor", "Shutdown requested")
+ Logger.i("Compositor", "Shutdown requested")
Quickshell.execDetached(["shutdown", "-h", "now"])
}
function reboot() {
- Logger.log("Compositor", "Reboot requested")
+ Logger.i("Compositor", "Reboot requested")
Quickshell.execDetached(["reboot"])
}
function suspend() {
- Logger.log("Compositor", "Suspend requested")
+ Logger.i("Compositor", "Suspend requested")
Quickshell.execDetached(["systemctl", "suspend"])
}
function lockAndSuspend() {
- Logger.log("Compositor", "Lock and suspend requested")
+ Logger.i("Compositor", "Lock and suspend requested")
try {
if (PanelService && PanelService.lockScreen && !PanelService.lockScreen.active) {
PanelService.lockScreen.active = true
}
} catch (e) {
- Logger.warn("Compositor", "Failed to activate lock screen before suspend: " + e)
+ Logger.w("Compositor", "Failed to activate lock screen before suspend: " + e)
}
// Queue suspend to the next event loop cycle to allow lock UI to render
Qt.callLater(suspend)
diff --git a/Services/ControlCenterWidgetRegistry.qml b/Services/ControlCenterWidgetRegistry.qml
index ce2fcbf1..f00263ed 100644
--- a/Services/ControlCenterWidgetRegistry.qml
+++ b/Services/ControlCenterWidgetRegistry.qml
@@ -49,7 +49,7 @@ Singleton {
}
function init() {
- Logger.log("ControlCenterWidgetRegistry", "Service started")
+ Logger.i("ControlCenterWidgetRegistry", "Service started")
}
// ------------------------------
diff --git a/Services/DarkModeService.qml b/Services/DarkModeService.qml
new file mode 100644
index 00000000..ad3eef00
--- /dev/null
+++ b/Services/DarkModeService.qml
@@ -0,0 +1,169 @@
+pragma Singleton
+
+import QtQuick
+import Quickshell
+import qs.Commons
+import qs.Services
+
+Singleton {
+ id: root
+
+ property bool initComplete: false
+ property bool nextDarkModeState: false
+
+ Connections {
+ target: LocationService.data
+ enabled: Settings.data.colorSchemes.schedulingMode == "location"
+ function onWeatherChanged() {
+ if (LocationService.data.weather !== null) {
+ const changes = root.collectWeatherChanges(LocationService.data.weather)
+ if (!root.initComplete) {
+ root.initComplete = true
+ root.applyCurrentMode(changes)
+ }
+ root.scheduleNextMode(changes)
+ }
+ }
+ }
+
+ Connections {
+ target: Settings.data.colorSchemes
+ enabled: Settings.data.colorSchemes.schedulingMode == "manual"
+ function onManualSunriseChanged() {
+ const changes = root.collectManualChanges()
+ root.applyCurrentMode(changes)
+ root.scheduleNextMode(changes)
+ }
+ function onManualSunsetChanged() {
+ const changes = root.collectManualChanges()
+ root.applyCurrentMode(changes)
+ root.scheduleNextMode(changes)
+ }
+ }
+
+ Connections {
+ target: Settings.data.colorSchemes
+ function onSchedulingModeChanged() {
+ root.init()
+ }
+ }
+
+ Timer {
+ id: timer
+ onTriggered: {
+ Settings.data.colorSchemes.darkMode = root.nextDarkModeState
+ if (LocationService.data.weather !== null) {
+ const changes = root.collectWeatherChanges(LocationService.data.weather)
+ root.scheduleNextMode(changes)
+ }
+ }
+ }
+
+ function init() {
+ Logger.i("DarkModeService", "Service started")
+
+ if (Settings.data.colorSchemes.schedulingMode == "manual") {
+ const changes = collectManualChanges()
+ initComplete = true
+ applyCurrentMode(changes)
+ scheduleNextMode(changes)
+ }
+
+ if (Settings.data.colorSchemes.schedulingMode == "location" && LocationService.data.weather) {
+ const changes = collectWeatherChanges(LocationService.data.weather)
+ initComplete = true
+ applyCurrentMode(changes)
+ scheduleNextMode(changes)
+ }
+ }
+
+ function parseTime(timeString) {
+ const parts = timeString.split(":").map(Number)
+ return {
+ "hour": parts[0],
+ "minute": parts[1]
+ }
+ }
+
+ function collectManualChanges() {
+ const sunriseTime = parseTime(Settings.data.colorSchemes.manualSunrise)
+ const sunsetTime = parseTime(Settings.data.colorSchemes.manualSunset)
+
+ const now = new Date()
+ const year = now.getFullYear()
+ const month = now.getMonth()
+ const day = now.getDate()
+
+ const yesterdaysSunset = new Date(year, month, day - 1, sunsetTime.hour, sunsetTime.minute)
+ const todaysSunrise = new Date(year, month, day, sunriseTime.hour, sunriseTime.minute)
+ const todaysSunset = new Date(year, month, day, sunsetTime.hour, sunsetTime.minute)
+ const tomorrowsSunrise = new Date(year, month, day + 1, sunriseTime.hour, sunriseTime.minute)
+
+ return [{
+ "time": yesterdaysSunset.getTime(),
+ "darkMode": true
+ }, {
+ "time": todaysSunrise.getTime(),
+ "darkMode": false
+ }, {
+ "time": todaysSunset.getTime(),
+ "darkMode": true
+ }, {
+ "time": tomorrowsSunrise.getTime(),
+ "darkMode": false
+ }]
+ }
+
+ function collectWeatherChanges(weather) {
+ const changes = []
+
+ if (Date.now() < Date.parse(weather.daily.sunrise[0])) {
+ // The sun has not risen yet
+ changes.push({
+ "time": Date.now() - 1,
+ "darkMode": true
+ })
+ }
+
+ for (var i = 0; i < weather.daily.sunrise.length; i++) {
+ changes.push({
+ "time": Date.parse(weather.daily.sunrise[i]),
+ "darkMode": false
+ })
+ changes.push({
+ "time": Date.parse(weather.daily.sunset[i]),
+ "darkMode": true
+ })
+ }
+
+ return changes
+ }
+
+ function applyCurrentMode(changes) {
+ const now = Date.now()
+
+ // changes.findLast(change => change.time < now) // not available in QML...
+ let lastChange = null
+ for (var i = 0; i < changes.length; i++) {
+ if (changes[i].time < now) {
+ lastChange = changes[i]
+ }
+ }
+
+ if (lastChange) {
+ Settings.data.colorSchemes.darkMode = lastChange.darkMode
+ Logger.d("DarkModeService", `Reset: darkmode=${lastChange.darkMode}`)
+ }
+ }
+
+ function scheduleNextMode(changes) {
+ const now = Date.now()
+ const nextChange = changes.find(change => change.time > now)
+ if (nextChange) {
+ root.nextDarkModeState = nextChange.darkMode
+ timer.interval = nextChange.time - now
+ timer.restart()
+ Logger.d("DarkModeService", `Scheduled: darkmode=${nextChange.darkMode} in ${timer.interval} ms`)
+ }
+ }
+}
diff --git a/Services/DistroLogoService.qml b/Services/DistroService.qml
similarity index 83%
rename from Services/DistroLogoService.qml
rename to Services/DistroService.qml
index 46f531b5..568bc2f0 100644
--- a/Services/DistroLogoService.qml
+++ b/Services/DistroService.qml
@@ -11,6 +11,8 @@ Singleton {
// Public properties
property string osPretty: ""
property string osLogo: ""
+ property bool isNixOS: false
+ property bool isReady: false
// Internal helpers
function buildCandidates(name) {
@@ -75,11 +77,15 @@ Singleton {
return l ? l.split("=")[1].replace(/"/g, "") : ""
}
root.osPretty = val("PRETTY_NAME") || val("NAME")
+ const osId = (val("ID") || "").toLowerCase()
+ root.isNixOS = osId === "nixos" || (root.osPretty || "").toLowerCase().includes("nixos")
const logoName = val("LOGO")
- if (logoName)
- resolveLogo(logoName)
+ if (logoName) {
+ resolveLogo(logoName)
+ }
+ root.isReady = true
} catch (e) {
- Logger.warn("DistroLogoService", "failed to read os-release", e)
+ Logger.w("DistroService", "failed to read os-release", e)
}
}
}
@@ -90,10 +96,10 @@ Singleton {
const p = String(stdout.text || "").trim()
if (code === 0 && p) {
root.osLogo = `file://${p}`
- Logger.log("DistroLogoService", "found", root.osLogo)
+ Logger.i("DistroService", "found", root.osLogo)
} else {
root.osLogo = ""
- Logger.warn("DistroLogoService", "none found")
+ Logger.w("DistroService", "none found")
}
}
stdout: StdioCollector {}
diff --git a/Services/FontService.qml b/Services/FontService.qml
index 08e07f78..1ffeee77 100644
--- a/Services/FontService.qml
+++ b/Services/FontService.qml
@@ -26,7 +26,7 @@ Singleton {
// -------------------------------------------
function init() {
- Logger.log("Font", "Service started")
+ Logger.i("Font", "Service started")
loadFontconfigMonospaceFonts()
}
@@ -39,7 +39,7 @@ Singleton {
if (isLoading)
return
- Logger.log("Font", "Loading system fonts...")
+ Logger.d("Font", "Loading system fonts...")
isLoading = true
var fontFamilies = Qt.fontFamilies()
@@ -125,7 +125,7 @@ Singleton {
fontsLoaded = true
isLoading = false
- Logger.log("Font", "Loaded", availableFonts.count, "fonts:", monospaceFonts.count, "monospace,", displayFonts.count, "display")
+ Logger.d("Font", "Loaded", availableFonts.count, "fonts:", monospaceFonts.count, "monospace,", displayFonts.count, "display")
}
function isMonospaceFont(fontName) {
diff --git a/Services/GitHubService.qml b/Services/GitHubService.qml
index 93982df9..1a2efc8a 100644
--- a/Services/GitHubService.qml
+++ b/Services/GitHubService.qml
@@ -53,11 +53,11 @@ Singleton {
function loadFromCache() {
const now = Time.timestamp
if (!data.timestamp || (now >= data.timestamp + githubUpdateFrequency)) {
- Logger.log("GitHub", "Cache expired or missing, fetching new data")
+ Logger.d("GitHub", "Cache expired or missing, fetching new data")
fetchFromGitHub()
return
}
- Logger.log("GitHub", "Loading cached GitHub data (age:", Math.round((now - data.timestamp) / 60), "minutes)")
+ Logger.d("GitHub", "Loading cached GitHub data (age:", Math.round((now - data.timestamp) / 60), "minutes)")
if (data.version) {
root.latestVersion = data.version
@@ -70,7 +70,7 @@ Singleton {
// --------------------------------
function fetchFromGitHub() {
if (isFetchingData) {
- Logger.warn("GitHub", "GitHub data is still fetching")
+ Logger.w("GitHub", "GitHub data is still fetching")
return
}
@@ -82,8 +82,8 @@ Singleton {
// --------------------------------
function saveData() {
data.timestamp = Time.timestamp
- Logger.log("GitHub", "Saving data to cache file:", githubDataFile)
- Logger.log("GitHub", "Data to save - version:", data.version, "contributors:", data.contributors.length)
+ Logger.d("GitHub", "Saving data to cache file:", githubDataFile)
+ Logger.d("GitHub", "Data to save - version:", data.version, "contributors:", data.contributors.length)
// Ensure cache directory exists
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir])
@@ -91,7 +91,7 @@ Singleton {
Qt.callLater(() => {
// Use direct ID reference to the FileView
githubDataFileView.writeAdapter()
- Logger.log("GitHub", "Cache file written successfully")
+ Logger.d("GitHub", "Cache file written successfully")
})
}
@@ -120,15 +120,15 @@ Singleton {
const version = data.tag_name
root.data.version = version
root.latestVersion = version
- Logger.log("GitHub", "Latest version fetched from GitHub:", version)
+ Logger.d("GitHub", "Latest version fetched from GitHub:", version)
} else {
- Logger.log("GitHub", "No tag_name in GitHub response")
+ Logger.w("GitHub", "No tag_name in GitHub response")
}
} else {
- Logger.log("GitHub", "Empty response from GitHub API")
+ Logger.w("GitHub", "Empty response from GitHub API")
}
} catch (e) {
- Logger.error("GitHub", "Failed to parse version:", e)
+ Logger.e("GitHub", "Failed to parse version:", e)
}
// Check if both processes are done
@@ -146,20 +146,20 @@ Singleton {
onStreamFinished: {
try {
const response = text
- Logger.log("GitHub", "Raw contributors response length:", response ? response.length : 0)
+ Logger.d("GitHub", "Raw contributors response length:", response ? response.length : 0)
if (response && response.trim()) {
const data = JSON.parse(response)
- Logger.log("GitHub", "Parsed contributors data type:", typeof data, "length:", Array.isArray(data) ? data.length : "not array")
+ Logger.d("GitHub", "Parsed contributors data type:", typeof data, "length:", Array.isArray(data) ? data.length : "not array")
root.data.contributors = data || []
root.contributors = root.data.contributors
- Logger.log("GitHub", "Contributors fetched from GitHub:", root.contributors.length)
+ Logger.d("GitHub", "Contributors fetched from GitHub:", root.contributors.length)
} else {
- Logger.log("GitHub", "Empty response from GitHub API for contributors")
+ Logger.w("GitHub", "Empty response from GitHub API for contributors")
root.data.contributors = []
root.contributors = []
}
} catch (e) {
- Logger.error("GitHub", "Failed to parse contributors:", e)
+ Logger.e("GitHub", "Failed to parse contributors:", e)
root.data.contributors = []
root.contributors = []
}
diff --git a/Services/HooksService.qml b/Services/HooksService.qml
index 4f04124a..6f6db9d3 100644
--- a/Services/HooksService.qml
+++ b/Services/HooksService.qml
@@ -38,9 +38,9 @@ Singleton {
let command = script.replace(/\$1/g, wallpaperPath)
command = command.replace(/\$2/g, screenName || "")
Quickshell.execDetached(["sh", "-c", command])
- Logger.log("HooksService", `Executed wallpaper hook: ${command}`)
+ Logger.d("HooksService", `Executed wallpaper hook: ${command}`)
} catch (e) {
- Logger.error("HooksService", `Failed to execute wallpaper hook: ${e}`)
+ Logger.e("HooksService", `Failed to execute wallpaper hook: ${e}`)
}
}
@@ -58,14 +58,14 @@ Singleton {
try {
const command = script.replace(/\$1/g, isDarkMode ? "true" : "false")
Quickshell.execDetached(["sh", "-c", command])
- Logger.log("HooksService", `Executed dark mode hook: ${command}`)
+ Logger.d("HooksService", `Executed dark mode hook: ${command}`)
} catch (e) {
- Logger.error("HooksService", `Failed to execute dark mode hook: ${e}`)
+ Logger.e("HooksService", `Failed to execute dark mode hook: ${e}`)
}
}
// Initialize the service
function init() {
- Logger.log("HooksService", "Service initialized")
+ Logger.i("HooksService", "Service initialized")
}
}
diff --git a/Services/HyprlandService.qml b/Services/HyprlandService.qml
index 0cafa965..b62d642c 100644
--- a/Services/HyprlandService.qml
+++ b/Services/HyprlandService.qml
@@ -45,9 +45,9 @@ Item {
queryDisplayScales()
})
initialized = true
- Logger.log("HyprlandService", "Initialized successfully")
+ Logger.i("HyprlandService", "Initialized successfully")
} catch (e) {
- Logger.error("HyprlandService", "Failed to initialize:", e)
+ Logger.e("HyprlandService", "Failed to initialize:", e)
}
}
@@ -74,7 +74,7 @@ Item {
onExited: function (exitCode) {
if (exitCode !== 0 || !accumulatedOutput) {
- Logger.error("HyprlandService", "Failed to query monitors, exit code:", exitCode)
+ Logger.e("HyprlandService", "Failed to query monitors, exit code:", exitCode)
accumulatedOutput = ""
return
}
@@ -105,7 +105,7 @@ Item {
CompositorService.onDisplayScalesUpdated(scales)
}
} catch (e) {
- Logger.error("HyprlandService", "Failed to parse monitors:", e)
+ Logger.e("HyprlandService", "Failed to parse monitors:", e)
} finally {
// Clear accumulated output for next query
accumulatedOutput = ""
@@ -152,7 +152,7 @@ Item {
workspaces.append(wsData)
}
} catch (e) {
- Logger.error("HyprlandService", "Error updating workspaces:", e)
+ Logger.e("HyprlandService", "Error updating workspaces:", e)
}
}
@@ -227,7 +227,7 @@ Item {
activeWindowChanged()
}
} catch (e) {
- Logger.error("HyprlandService", "Error updating windows:", e)
+ Logger.e("HyprlandService", "Error updating windows:", e)
}
}
@@ -242,8 +242,8 @@ Item {
if (!windowId)
return null
- const appId = extractAppId(toplevel)
- const title = safeGetProperty(toplevel, "title", "")
+ const appId = getAppId(toplevel)
+ const title = getAppTitle(toplevel)
const wsId = toplevel.workspace ? toplevel.workspace.id : null
const focused = toplevel.activated === true
const output = toplevel.monitor?.name || ""
@@ -261,13 +261,37 @@ Item {
}
}
- // Extract app ID from various possible sources
- function extractAppId(toplevel) {
+ function getAppTitle(toplevel) {
+ try {
+ var title = toplevel.wayland.title
+ if (title)
+ return title
+ } catch (e) {
+
+ }
+
+ return safeGetProperty(toplevel, "title", "")
+ }
+
+ function getAppId(toplevel) {
if (!toplevel)
return ""
+ var appId = ""
+
+ // Try the wayland object first!
+ // From my (Lemmy) testing it works fine so we could probably get rid of all the other attempts below.
+ // Leaving them in for now, just in case...
+ try {
+ appId = toplevel.wayland.appId
+ if (appId)
+ return appId
+ } catch (e) {
+
+ }
+
// Try direct properties
- var appId = safeGetProperty(toplevel, "class", "")
+ appId = safeGetProperty(toplevel, "class", "")
if (appId)
return appId
@@ -287,7 +311,6 @@ Item {
}
} catch (e) {
- // Ignore IPC errors
}
return ""
@@ -329,6 +352,7 @@ Item {
target: Hyprland
enabled: initialized
function onRawEvent(event) {
+ Hyprland.refreshWorkspaces()
safeUpdateWorkspaces()
workspaceChanged()
updateTimer.restart()
@@ -345,7 +369,7 @@ Item {
try {
Hyprland.dispatch(`workspace ${workspace.idx}`)
} catch (e) {
- Logger.error("HyprlandService", "Failed to switch workspace:", e)
+ Logger.e("HyprlandService", "Failed to switch workspace:", e)
}
}
@@ -353,7 +377,7 @@ Item {
try {
Hyprland.dispatch(`focuswindow address:0x${window.id.toString()}`)
} catch (e) {
- Logger.error("HyprlandService", "Failed to switch window:", e)
+ Logger.e("HyprlandService", "Failed to switch window:", e)
}
}
@@ -361,7 +385,7 @@ Item {
try {
Hyprland.dispatch(`killwindow address:0x${window.id}`)
} catch (e) {
- Logger.error("HyprlandService", "Failed to close window:", e)
+ Logger.e("HyprlandService", "Failed to close window:", e)
}
}
@@ -369,7 +393,7 @@ Item {
try {
Quickshell.execDetached(["hyprctl", "dispatch", "exit"])
} catch (e) {
- Logger.error("HyprlandService", "Failed to logout:", e)
+ Logger.e("HyprlandService", "Failed to logout:", e)
}
}
}
diff --git a/Services/IPCService.qml b/Services/IPCService.qml
index e0f2eac0..3d1d3924 100644
--- a/Services/IPCService.qml
+++ b/Services/IPCService.qml
@@ -215,6 +215,10 @@ Item {
MediaService.play()
}
+ function stop() {
+ MediaService.stop()
+ }
+
function pause() {
MediaService.pause()
}
@@ -230,7 +234,7 @@ Item {
function seekRelative(offset: string) {
var offsetVal = parseFloat(position)
if (Number.isNaN(offsetVal)) {
- Logger.warn("Media", "Argument to ipc call 'media seekRelative' must be a number")
+ Logger.w("Media", "Argument to ipc call 'media seekRelative' must be a number")
return
}
MediaService.seekRelative(offsetVal)
@@ -239,7 +243,7 @@ Item {
function seekByRatio(position: string) {
var positionVal = parseFloat(position)
if (Number.isNaN(positionVal)) {
- Logger.warn("Media", "Argument to ipc call 'media seekByRatio' must be a number")
+ Logger.w("Media", "Argument to ipc call 'media seekByRatio' must be a number")
return
}
MediaService.seekByRatio(positionVal)
diff --git a/Services/IdleInhibitorService.qml b/Services/IdleInhibitorService.qml
index a1f789d0..ab20dd94 100644
--- a/Services/IdleInhibitorService.qml
+++ b/Services/IdleInhibitorService.qml
@@ -16,15 +16,9 @@ Singleton {
// Different inhibitor strategies
property string strategy: "systemd" // "systemd", "wayland", or "auto"
- Component.onCompleted: {
- Logger.log("IdleInhibitor", "Service started")
+ function init() {
+ Logger.d("IdleInhibitor", "Service started")
detectStrategy()
-
- // Restore previous state from settings
- if (Settings.data.ui.idleInhibitorEnabled) {
- addInhibitor("manual", "Restored from previous session")
- Logger.log("IdleInhibitor", "Restored previous manual inhibition state")
- }
}
// Auto-detect the best strategy
@@ -34,7 +28,7 @@ Singleton {
try {
var systemdResult = Quickshell.execDetached(["which", "systemd-inhibit"])
strategy = "systemd"
- Logger.log("IdleInhibitor", "Using systemd-inhibit strategy")
+ Logger.d("IdleInhibitor", "Using systemd-inhibit strategy")
return
} catch (e) {
@@ -44,14 +38,14 @@ Singleton {
try {
var waylandResult = Quickshell.execDetached(["which", "wayhibitor"])
strategy = "wayland"
- Logger.log("IdleInhibitor", "Using wayhibitor strategy")
+ Logger.d("IdleInhibitor", "Using wayhibitor strategy")
return
} catch (e) {
// wayhibitor not found
}
- Logger.warn("IdleInhibitor", "No suitable inhibitor found - will try systemd as fallback")
+ Logger.w("IdleInhibitor", "No suitable inhibitor found - will try systemd as fallback")
strategy = "systemd" // Fallback to systemd even if not detected
}
}
@@ -59,13 +53,13 @@ Singleton {
// Add an inhibitor
function addInhibitor(id, reason = "Application request") {
if (activeInhibitors.includes(id)) {
- Logger.warn("IdleInhibitor", "Inhibitor already active:", id)
+ Logger.w("IdleInhibitor", "Inhibitor already active:", id)
return false
}
activeInhibitors.push(id)
updateInhibition(reason)
- Logger.log("IdleInhibitor", "Added inhibitor:", id)
+ Logger.d("IdleInhibitor", "Added inhibitor:", id)
return true
}
@@ -73,13 +67,13 @@ Singleton {
function removeInhibitor(id) {
const index = activeInhibitors.indexOf(id)
if (index === -1) {
- Logger.warn("IdleInhibitor", "Inhibitor not found:", id)
+ Logger.w("IdleInhibitor", "Inhibitor not found:", id)
return false
}
activeInhibitors.splice(index, 1)
updateInhibition()
- Logger.log("IdleInhibitor", "Removed inhibitor:", id)
+ Logger.d("IdleInhibitor", "Removed inhibitor:", id)
return true
}
@@ -108,12 +102,12 @@ Singleton {
} else if (strategy === "wayland") {
startWaylandInhibition()
} else {
- Logger.warn("IdleInhibitor", "No inhibition strategy available")
+ Logger.w("IdleInhibitor", "No inhibition strategy available")
return
}
isInhibited = true
- Logger.log("IdleInhibitor", "Started inhibition:", reason)
+ Logger.i("IdleInhibitor", "Started inhibition:", reason)
}
// Stop system inhibition
@@ -126,7 +120,7 @@ Singleton {
}
isInhibited = false
- Logger.log("IdleInhibitor", "Stopped inhibition")
+ Logger.i("IdleInhibitor", "Stopped inhibition")
}
// Systemd inhibition using systemd-inhibit
@@ -148,13 +142,13 @@ Singleton {
onExited: function (exitCode, exitStatus) {
if (isInhibited) {
- Logger.warn("IdleInhibitor", "Inhibitor process exited unexpectedly:", exitCode)
+ Logger.w("IdleInhibitor", "Inhibitor process exited unexpectedly:", exitCode)
isInhibited = false
}
}
onStarted: function () {
- Logger.log("IdleInhibitor", "Inhibitor process started successfully")
+ Logger.d("IdleInhibitor", "Inhibitor process started successfully")
}
}
@@ -162,15 +156,13 @@ Singleton {
function manualToggle() {
if (activeInhibitors.includes("manual")) {
removeInhibitor("manual")
- Settings.data.ui.idleInhibitorEnabled = false
ToastService.showNotice(I18n.tr("tooltips.keep-awake"), I18n.tr("toast.keep-awake.disabled"))
- Logger.log("IdleInhibitor", "Manual inhibition disabled and saved to settings")
+ Logger.i("IdleInhibitor", "Manual inhibition disabled")
return false
} else {
addInhibitor("manual", "Manually activated by user")
- Settings.data.ui.idleInhibitorEnabled = true
ToastService.showNotice(I18n.tr("tooltips.keep-awake"), I18n.tr("toast.keep-awake.enabled"))
- Logger.log("IdleInhibitor", "Manual inhibition enabled and saved to settings")
+ Logger.i("IdleInhibitor", "Manual inhibition enabled (will reset on next session)")
return true
}
}
diff --git a/Services/KeyboardLayoutService.qml b/Services/KeyboardLayoutService.qml
index 3006d121..ed0b4f8b 100644
--- a/Services/KeyboardLayoutService.qml
+++ b/Services/KeyboardLayoutService.qml
@@ -202,7 +202,7 @@ Singleton {
}
Component.onCompleted: {
- Logger.log("KeyboardLayout", "Service started")
+ Logger.i("KeyboardLayout", "Service started")
updateLayout()
}
diff --git a/Services/LocationService.qml b/Services/LocationService.qml
index 79b4aa58..7c2df5d4 100644
--- a/Services/LocationService.qml
+++ b/Services/LocationService.qml
@@ -28,14 +28,14 @@ Singleton {
printErrors: false
onAdapterUpdated: saveTimer.start()
onLoaded: {
- Logger.log("Location", "Loaded cached data")
+ Logger.d("Location", "Loaded cached data")
// Initialize stable properties on load
if (adapter.latitude !== "" && adapter.longitude !== "" && adapter.weatherLastFetch > 0) {
root.stableLatitude = adapter.latitude
root.stableLongitude = adapter.longitude
root.stableName = adapter.name
root.coordinatesReady = true
- Logger.log("Location", "Coordinates ready")
+ Logger.i("Location", "Coordinates ready")
}
updateWeather()
}
@@ -69,7 +69,7 @@ Singleton {
Timer {
id: updateTimer
interval: 20 * 1000
- running: true
+ running: Settings.data.location.weatherEnabled || Settings.data.colorSchemes.schedulingMode == "location"
repeat: true
onTriggered: {
updateWeather()
@@ -87,12 +87,12 @@ Singleton {
function init() {
// does nothing but ensure the singleton is created
// do not remove
- Logger.log("Location", "Service started")
+ Logger.i("Location", "Service started")
}
// --------------------------------
function resetWeather() {
- Logger.log("Location", "Resetting weather data")
+ Logger.i("Location", "Resetting weather data")
// Mark as changing to prevent UI updates
root.coordinatesReady = false
@@ -114,8 +114,12 @@ Singleton {
// --------------------------------
function updateWeather() {
+ if (!Settings.data.location.weatherEnabled) {
+ return
+ }
+
if (isFetchingWeather) {
- Logger.warn("Location", "Weather is still fetching")
+ Logger.w("Location", "Weather is still fetching")
return
}
@@ -132,13 +136,13 @@ Singleton {
const locationChanged = data.name !== Settings.data.location.name
if (locationChanged) {
root.coordinatesReady = false
- Logger.log("Location", "Location changed from", adapter.name, "to", Settings.data.location.name)
+ Logger.d("Location", "Location changed from", adapter.name, "to", Settings.data.location.name)
}
if ((adapter.latitude === "") || (adapter.longitude === "") || locationChanged) {
_geocodeLocation(Settings.data.location.name, function (latitude, longitude, name, country) {
- Logger.log("Location", "Geocoded", Settings.data.location.name, "to:", latitude, "/", longitude)
+ Logger.d("Location", "Geocoded", Settings.data.location.name, "to:", latitude, "/", longitude)
// Save location name
adapter.name = Settings.data.location.name
@@ -158,7 +162,7 @@ Singleton {
// --------------------------------
function _geocodeLocation(locationName, callback, errorCallback) {
- Logger.log("Location", "Geocoding location name")
+ Logger.d("Location", "Geocoding location name")
var geoUrl = "https://assets.noctalia.dev/geocode.php?city=" + encodeURIComponent(locationName) + "&language=en&format=json"
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
@@ -185,8 +189,8 @@ Singleton {
// --------------------------------
function _fetchWeather(latitude, longitude, errorCallback) {
- Logger.log("Location", "Fetching weather from api.open-meteo.com")
- var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto"
+ Logger.d("Location", "Fetching weather from api.open-meteo.com")
+ var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode,sunset,sunrise&timezone=auto"
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
@@ -205,7 +209,7 @@ Singleton {
root.coordinatesReady = true
isFetchingWeather = false
- Logger.log("Location", "Cached weather to disk - stable coordinates updated")
+ Logger.i("Location", "Cached weather to disk - stable coordinates updated")
} catch (e) {
errorCallback("Location", "Failed to parse weather data")
}
@@ -220,7 +224,7 @@ Singleton {
// --------------------------------
function errorCallback(module, message) {
- Logger.error(module, message)
+ Logger.e(module, message)
isFetchingWeather = false
}
diff --git a/Services/MatugenTemplates.qml b/Services/MatugenTemplates.qml
index c7da9c3c..3475e444 100644
--- a/Services/MatugenTemplates.qml
+++ b/Services/MatugenTemplates.qml
@@ -2,6 +2,7 @@ pragma Singleton
import QtQuick
import Quickshell
+import Quickshell.Io
import qs.Commons
// Central place to define which templates we generate and where they write.
@@ -32,6 +33,50 @@ Singleton {
return ""
}
+ // Build user templates TOML for ~/.config/noctalia/user-templates.toml
+ function buildUserTemplatesToml() {
+ var lines = []
+ lines.push("[config]")
+ lines.push("")
+ lines.push("# User-defined templates")
+ lines.push("# Add your custom templates below")
+ lines.push("# Example:")
+ lines.push("# [templates.myapp]")
+ lines.push("# input_path = \"~/.config/noctalia/templates/myapp.css\"")
+ lines.push("# output_path = \"~/.config/myapp/theme.css\"")
+ lines.push("# post_hook = \"myapp --reload-theme\"")
+ lines.push("")
+ lines.push("# Remove this section and add your own templates")
+ lines.push("#[templates.placeholder]")
+ lines.push("#input_path = \"" + Quickshell.shellDir + "/Assets/MatugenTemplates/noctalia.json\"")
+ lines.push("#output_path = \"" + Settings.cacheDir + "placeholder.json\"")
+ lines.push("")
+
+ return lines.join("\n") + "\n"
+ }
+
+ // Write user templates TOML to ~/.config/noctalia/user-templates.toml
+ function writeUserTemplatesToml() {
+ var userConfigPath = Settings.configDir + "user-templates.toml"
+
+ // Check if file already exists
+ fileCheckProcess.command = ["test", "-f", userConfigPath]
+ fileCheckProcess.running = true
+ }
+
+ function doWriteUserTemplatesToml() {
+ var userConfigPath = Settings.configDir + "user-templates.toml"
+ var configContent = buildUserTemplatesToml()
+
+ // Ensure directory exists (should already exist but just in case)
+ Quickshell.execDetached(["mkdir", "-p", Settings.configDir])
+
+ // Write the config file
+ Quickshell.execDetached(["sh", "-c", `echo '${configContent.replace(/'/g, "'\\''")}' > '${userConfigPath}'`])
+
+ Logger.d("MatugenTemplates", "User templates config written to:", userConfigPath)
+ }
+
// --------------------------------
function addWallpaperBasedTemplates(lines, mode) {
// Noctalia colors
@@ -156,6 +201,14 @@ Singleton {
"output": "~/.config/dorion/themes/noctalia.theme.css"
}],
"input": "vesktop.css"
+ }, {
+ "name": "discord_vencord",
+ "templates": [{
+ "version": "discord_vencord",
+ "output": "~/.config/discord/themes/noctalia.theme.css"
+ }],
+ "input": "vesktop.css",
+ "requiresThemesFolder": true
}]
// --------------------------------
@@ -193,10 +246,27 @@ Singleton {
clients.push({
"name": clientName,
"configPath": configPath,
- "themePath": themePath
+ "themePath": themePath,
+ "requiresThemesFolder": app.requiresThemesFolder || false
})
}
}
return clients
}
+
+ // Process for checking if user templates file exists
+ Process {
+ id: fileCheckProcess
+ running: false
+
+ onExited: function (exitCode) {
+ if (exitCode === 0) {
+ // File exists, skip creation
+ Logger.d("MatugenTemplates", "User templates config already exists, skipping creation")
+ } else {
+ // File doesn't exist, create it
+ doWriteUserTemplatesToml()
+ }
+ }
+ }
}
diff --git a/Services/MediaService.qml b/Services/MediaService.qml
index 006285c9..30295bc5 100644
--- a/Services/MediaService.qml
+++ b/Services/MediaService.qml
@@ -38,12 +38,19 @@ Singleton {
let allPlayers = Mpris.players.values
let finalPlayers = []
const genericBrowsers = ["firefox", "chromium", "chrome"]
+ const blacklist = (Settings.data.audio && Settings.data.audio.mprisBlacklist) ? Settings.data.audio.mprisBlacklist : []
// Separate players into specific and generic lists
let specificPlayers = []
let genericPlayers = []
for (var i = 0; i < allPlayers.length; i++) {
const identity = String(allPlayers[i].identity || "").toLowerCase()
+ const match = blacklist.find(b => {
+ const s = String(b || "").toLowerCase()
+ return s && (identity.includes(s))
+ })
+ if (match)
+ continue
if (genericBrowsers.some(b => identity.includes(b))) {
genericPlayers.push(allPlayers[i])
} else {
@@ -129,14 +136,14 @@ Singleton {
function findActivePlayer() {
let availablePlayers = getAvailablePlayers()
if (availablePlayers.length === 0) {
- //Logger.log("Media", "No active player found")
+ //Logger.i("Media", "No active player found")
return null
}
// Prioritize the actively playing player ---
for (var i = 0; i < availablePlayers.length; i++) {
if (availablePlayers[i] && availablePlayers[i].playbackState === MprisPlaybackState.Playing) {
- Logger.log("Media", "Found actively playing player: " + availablePlayers[i].identity)
+ Logger.d("Media", "Found actively playing player: " + availablePlayers[i].identity)
selectedPlayerIndex = i
return availablePlayers[i]
}
@@ -170,7 +177,7 @@ Singleton {
if (newPlayer !== currentPlayer) {
currentPlayer = newPlayer
currentPosition = currentPlayer ? currentPlayer.position : 0
- Logger.log("Media", "Switching player")
+ Logger.d("Media", "Switching player")
}
}
@@ -193,6 +200,13 @@ Singleton {
}
}
+ function stop() {
+ let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
+ if (target) {
+ target.stop()
+ }
+ }
+
function pause() {
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
if (target && target.canPause) {
@@ -286,7 +300,7 @@ Singleton {
Connections {
target: Mpris.players
function onValuesChanged() {
- Logger.log("Media", "Players changed")
+ Logger.d("Media", "Players changed")
updateCurrentPlayer()
}
}
diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml
index 0caaf025..4e5d6dfb 100644
--- a/Services/NetworkService.qml
+++ b/Services/NetworkService.qml
@@ -56,7 +56,7 @@ Singleton {
}
Component.onCompleted: {
- Logger.log("Network", "Service initialized")
+ Logger.i("Network", "Service initialized")
syncWifiState()
scan()
}
@@ -105,7 +105,7 @@ Singleton {
if (scanning) {
// Mark current scan results to be ignored and schedule a new scan
- Logger.log("Network", "Scan already in progress, will ignore results and rescan")
+ Logger.d("Network", "Scan already in progress, will ignore results and rescan")
ignoreScanResults = true
scanPending = true
return
@@ -117,7 +117,7 @@ Singleton {
// Get existing profiles first, then scan
profileCheckProcess.running = true
- Logger.log("Network", "Wi-Fi scan in progress...")
+ Logger.d("Network", "Wi-Fi scan in progress...")
}
function connect(ssid, password = "") {
@@ -229,7 +229,7 @@ Singleton {
})
if (root.ethernetConnected !== connected) {
root.ethernetConnected = connected
- Logger.log("Network", "Ethernet connected:", root.ethernetConnected)
+ Logger.d("Network", "Ethernet connected:", root.ethernetConnected)
}
}
}
@@ -245,7 +245,7 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
const enabled = text.trim() === "enabled"
- Logger.log("Network", "Wi-Fi adapter was detect as enabled:", enabled)
+ Logger.d("Network", "Wi-Fi adapter was detect as enabled:", enabled)
if (Settings.data.network.wifiEnabled !== enabled) {
Settings.data.network.wifiEnabled = enabled
}
@@ -261,7 +261,7 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
- Logger.log("Network", "Wi-Fi state change command executed.")
+ Logger.i("Network", "Wi-Fi state change command executed.")
// Re-check the state to ensure it's in sync
syncWifiState()
}
@@ -270,7 +270,7 @@ Singleton {
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
- Logger.warn("Network", "Error changing Wi-Fi state: " + text)
+ Logger.w("Network", "Error changing Wi-Fi state: " + text)
}
}
}
@@ -285,7 +285,7 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
if (root.ignoreScanResults) {
- Logger.log("Network", "Ignoring profile check results (new scan requested)")
+ Logger.d("Network", "Ignoring profile check results (new scan requested)")
root.scanning = false
// Check if we need to start a new scan
@@ -318,7 +318,7 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
if (root.ignoreScanResults) {
- Logger.log("Network", "Ignoring scan results (new scan requested)")
+ Logger.d("Network", "Ignoring scan results (new scan requested)")
root.scanning = false
// Check if we need to start a new scan
@@ -344,7 +344,7 @@ Singleton {
// We know the last 3 fields, so everything else is SSID
const lastColonIdx = line.lastIndexOf(":")
if (lastColonIdx === -1) {
- Logger.warn("Network", "Malformed nmcli output line:", line)
+ Logger.w("Network", "Malformed nmcli output line:", line)
continue
}
@@ -353,7 +353,7 @@ Singleton {
const secondLastColonIdx = remainingLine.lastIndexOf(":")
if (secondLastColonIdx === -1) {
- Logger.warn("Network", "Malformed nmcli output line:", line)
+ Logger.w("Network", "Malformed nmcli output line:", line)
continue
}
@@ -362,7 +362,7 @@ Singleton {
const thirdLastColonIdx = remainingLine2.lastIndexOf(":")
if (thirdLastColonIdx === -1) {
- Logger.warn("Network", "Malformed nmcli output line:", line)
+ Logger.w("Network", "Malformed nmcli output line:", line)
continue
}
@@ -410,15 +410,15 @@ Singleton {
if (newNetworks.length > 0 || lostNetworks.length > 0) {
if (newNetworks.length > 0) {
- Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", "))
+ Logger.d("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", "))
}
if (lostNetworks.length > 0) {
- Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", "))
+ Logger.d("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", "))
}
- Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(networksMap).length)
+ Logger.d("Network", "Total Wi-Fi SSIDs:", Object.keys(networksMap).length)
}
- Logger.log("Network", "Wi-Fi scan completed")
+ Logger.d("Network", "Wi-Fi scan completed")
root.networks = networksMap
root.scanning = false
@@ -435,7 +435,7 @@ Singleton {
onStreamFinished: {
root.scanning = false
if (text.trim()) {
- Logger.warn("Network", "Scan error: " + text)
+ Logger.w("Network", "Scan error: " + text)
// If scan fails, retry
delayedScanTimer.interval = 5000
@@ -491,7 +491,7 @@ Singleton {
root.connecting = false
root.connectingTo = ""
- Logger.log("Network", `Connected to network: '${connectProcess.ssid}'`)
+ Logger.i("Network", `Connected to network: '${connectProcess.ssid}'`)
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("toast.wifi.connected", {
"ssid": connectProcess.ssid
}))
@@ -520,7 +520,7 @@ Singleton {
root.lastError = text.split("\n")[0].trim()
}
- Logger.warn("Network", "Connect error: " + text)
+ Logger.w("Network", "Connect error: " + text)
}
}
}
@@ -534,7 +534,7 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
- Logger.log("Network", `Disconnected from network: '${disconnectProcess.ssid}'`)
+ Logger.i("Network", `Disconnected from network: '${disconnectProcess.ssid}'`)
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("toast.wifi.disconnected", {
"ssid": disconnectProcess.ssid
}))
@@ -553,7 +553,7 @@ Singleton {
onStreamFinished: {
root.disconnectingFrom = ""
if (text.trim()) {
- Logger.warn("Network", "Disconnect error: " + text)
+ Logger.w("Network", "Disconnect error: " + text)
}
// Still trigger a scan even on error
delayedScanTimer.interval = 5000
@@ -599,8 +599,8 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
- Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`)
- Logger.log("Network", text.trim().replace(/[\r\n]/g, " "))
+ Logger.i("Network", `Forget network: "${forgetProcess.ssid}"`)
+ Logger.d("Network", text.trim().replace(/[\r\n]/g, " "))
// Update both cached and existing status immediately
let nets = root.networks
@@ -624,7 +624,7 @@ Singleton {
onStreamFinished: {
root.forgettingNetwork = ""
if (text.trim() && !text.includes("No profiles found")) {
- Logger.warn("Network", "Forget error: " + text)
+ Logger.w("Network", "Forget error: " + text)
}
// Still Trigger a scan even on error
delayedScanTimer.interval = 5000
diff --git a/Services/NightLightService.qml b/Services/NightLightService.qml
index a52be4d8..adf3704e 100644
--- a/Services/NightLightService.qml
+++ b/Services/NightLightService.qml
@@ -93,10 +93,10 @@ Singleton {
id: runner
running: false
onStarted: {
- Logger.log("NightLight", "Wlsunset started:", runner.command)
+ Logger.i("NightLight", "Wlsunset started:", runner.command)
}
onExited: function (code, status) {
- Logger.log("NightLight", "Wlsunset exited:", code, status)
+ Logger.i("NightLight", "Wlsunset exited:", code, status)
}
}
}
diff --git a/Services/NiriService.qml b/Services/NiriService.qml
index 16226348..ab89d366 100644
--- a/Services/NiriService.qml
+++ b/Services/NiriService.qml
@@ -28,7 +28,7 @@ Item {
updateWorkspaces()
updateWindows()
queryDisplayScales()
- Logger.log("NiriService", "Initialized successfully")
+ Logger.i("NiriService", "Initialized successfully")
}
// Update workspaces
@@ -89,7 +89,7 @@ Item {
CompositorService.onDisplayScalesUpdated(scales)
}
} catch (e) {
- Logger.error("NiriService", "Failed to parse outputs:", e, line)
+ Logger.e("NiriService", "Failed to parse outputs:", e, line)
}
}
}
@@ -136,7 +136,7 @@ Item {
workspaceChanged()
} catch (e) {
- Logger.error("NiriService", "Failed to parse workspaces:", e, line)
+ Logger.e("NiriService", "Failed to parse workspaces:", e, line)
}
}
}
@@ -154,7 +154,7 @@ Item {
const windowsData = JSON.parse(line)
recollectWindows(windowsData)
} catch (e) {
- Logger.error("NiriService", "Failed to parse windows:", e, line)
+ Logger.e("NiriService", "Failed to parse windows:", e, line)
}
}
}
@@ -193,7 +193,7 @@ Item {
queryDisplayScales()
}
} catch (e) {
- Logger.error("NiriService", "Error parsing event stream:", e, data)
+ Logger.e("NiriService", "Error parsing event stream:", e, data)
}
}
}
@@ -300,7 +300,7 @@ Item {
windowListChanged()
} catch (e) {
- Logger.error("NiriService", "Error handling WindowOpenedOrChanged:", e)
+ Logger.e("NiriService", "Error handling WindowOpenedOrChanged:", e)
}
}
@@ -324,7 +324,7 @@ Item {
windowListChanged()
}
} catch (e) {
- Logger.error("NiriService", "Error handling WindowClosed:", e)
+ Logger.e("NiriService", "Error handling WindowClosed:", e)
}
}
@@ -333,7 +333,7 @@ Item {
const windowsData = eventData.windows
recollectWindows(windowsData)
} catch (e) {
- Logger.error("NiriService", "Error handling WindowsChanged:", e)
+ Logger.e("NiriService", "Error handling WindowsChanged:", e)
}
}
@@ -359,7 +359,7 @@ Item {
activeWindowChanged()
} catch (e) {
- Logger.error("NiriService", "Error handling WindowFocusChanged:", e)
+ Logger.e("NiriService", "Error handling WindowFocusChanged:", e)
}
}
@@ -378,16 +378,16 @@ Item {
windowListChanged()
} catch (e) {
- Logger.error("NiriService", "Error handling WindowLayoutChanged:", e)
+ Logger.e("NiriService", "Error handling WindowLayoutChanged:", e)
}
}
function handleOverviewOpenedOrClosed(eventData) {
try {
overviewActive = eventData.is_open
- Logger.log("NiriService", "Overview opened or closed:", eventData.is_open)
+ Logger.d("NiriService", "Overview opened or closed:", eventData.is_open)
} catch (e) {
- Logger.error("NiriService", "Error handling OverviewOpenedOrClosed:", e)
+ Logger.e("NiriService", "Error handling OverviewOpenedOrClosed:", e)
}
}
@@ -396,7 +396,7 @@ Item {
try {
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspace.idx.toString()])
} catch (e) {
- Logger.error("NiriService", "Failed to switch workspace:", e)
+ Logger.e("NiriService", "Failed to switch workspace:", e)
}
}
@@ -404,7 +404,7 @@ Item {
try {
Quickshell.execDetached(["niri", "msg", "action", "focus-window", "--id", window.id.toString()])
} catch (e) {
- Logger.error("NiriService", "Failed to switch window:", e)
+ Logger.e("NiriService", "Failed to switch window:", e)
}
}
@@ -412,7 +412,7 @@ Item {
try {
Quickshell.execDetached(["niri", "msg", "action", "close-window", "--id", window.id.toString()])
} catch (e) {
- Logger.error("NiriService", "Failed to close window:", e)
+ Logger.e("NiriService", "Failed to close window:", e)
}
}
@@ -420,7 +420,7 @@ Item {
try {
Quickshell.execDetached(["niri", "msg", "action", "quit", "--skip-confirmation"])
} catch (e) {
- Logger.error("NiriService", "Failed to logout:", e)
+ Logger.e("NiriService", "Failed to logout:", e)
}
}
}
diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml
index 249a5845..4bb5042d 100644
--- a/Services/NotificationService.qml
+++ b/Services/NotificationService.qml
@@ -111,7 +111,9 @@ Singleton {
notificationMetadata[data.id] = {
"timestamp": data.timestamp.getTime(),
"duration": expire,
- "urgency": data.urgency
+ "urgency": data.urgency,
+ "paused": false,
+ "pauseTime": 0
}
activeList.insert(0, data)
@@ -140,7 +142,7 @@ Singleton {
"id": id,
"summary": (n.summary || ""),
"body": stripTags(n.body || ""),
- "appName": getAppName(n.appName),
+ "appName": getAppName(n.appName || n.desktopEntry || ""),
"urgency": n.urgency < 0 || n.urgency > 2 ? 1 : n.urgency,
"expireTimeout": n.expireTimeout,
"timestamp": time,
@@ -219,7 +221,7 @@ Singleton {
const notif = activeList.get(i)
const meta = notificationMetadata[notif.id]
- if (!meta || meta.duration === -1)
+ if (!meta || meta.duration === -1 || meta.paused)
continue
// Skip infinite notifications
@@ -301,7 +303,7 @@ Singleton {
adapter.notifications = items
historyFileView.writeAdapter()
} catch (e) {
- Logger.error("Notifications", "Save history failed:", e)
+ Logger.e("Notifications", "Save history failed:", e)
}
}
@@ -331,7 +333,7 @@ Singleton {
})
}
} catch (e) {
- Logger.error("Notifications", "Load failed:", e)
+ Logger.e("Notifications", "Load failed:", e)
}
}
@@ -408,6 +410,22 @@ Singleton {
return ""
}
+ function pauseTimeout(id) {
+ const meta = notificationMetadata[id]
+ if (meta && !meta.paused) {
+ meta.paused = true
+ meta.pauseTime = Date.now()
+ }
+ }
+
+ function resumeTimeout(id) {
+ const meta = notificationMetadata[id]
+ if (meta && meta.paused) {
+ meta.timestamp += Date.now() - meta.pauseTime
+ meta.paused = false
+ }
+ }
+
// Public API
function dismissActiveNotification(id) {
activeMap[id]?.dismiss()
@@ -461,7 +479,7 @@ Singleton {
try {
Quickshell.execDetached(["sh", "-c", `rm -rf "${Settings.cacheDirImagesNotifications}"*`])
} catch (e) {
- Logger.error("Notifications", "Failed to clear cache directory:", e)
+ Logger.e("Notifications", "Failed to clear cache directory:", e)
}
historyList.clear()
diff --git a/Services/PanelService.qml b/Services/PanelService.qml
index 29320c1d..1ee055c1 100644
--- a/Services/PanelService.qml
+++ b/Services/PanelService.qml
@@ -24,7 +24,7 @@ Singleton {
// Register this panel
function registerPanel(panel) {
registeredPanels[panel.objectName] = panel
- Logger.log("PanelService", "Registered:", panel.objectName)
+ Logger.d("PanelService", "Registered:", panel.objectName)
}
// Returns a panel
diff --git a/Services/PowerProfileService.qml b/Services/PowerProfileService.qml
index 9a43217e..8aa7ac64 100644
--- a/Services/PowerProfileService.qml
+++ b/Services/PowerProfileService.qml
@@ -55,7 +55,7 @@ Singleton {
try {
powerProfiles.profile = p
} catch (e) {
- Logger.error("PowerProfileService", "Failed to set profile:", e)
+ Logger.e("PowerProfileService", "Failed to set profile:", e)
}
}
diff --git a/Services/ProgramCheckerService.qml b/Services/ProgramCheckerService.qml
index 268274e3..790fa71a 100644
--- a/Services/ProgramCheckerService.qml
+++ b/Services/ProgramCheckerService.qml
@@ -18,6 +18,7 @@ Singleton {
property bool fuzzelAvailable: false
property bool gpuScreenRecorderAvailable: false
property bool wlsunsetAvailable: false
+ property bool app2unitAvailable: false
// Discord client auto-detection
property var availableDiscordClients: []
@@ -27,14 +28,25 @@ Singleton {
// Function to detect Discord client by checking config directories
function detectDiscordClient() {
- // Build list of client names from MatugenTemplates
- var clientNames = []
+ // Build shell script to check each client
+ var scriptParts = ["available_clients=\"\";"]
+
for (var i = 0; i < MatugenTemplates.discordClients.length; i++) {
- clientNames.push(MatugenTemplates.discordClients[i].name)
+ var client = MatugenTemplates.discordClients[i]
+ var clientName = client.name
+
+ // Check if this client requires themes folder to exist
+ if (client.requiresThemesFolder) {
+ scriptParts.push("if [ -d \"$HOME/.config/" + clientName + "/themes\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;")
+ } else {
+ scriptParts.push("if [ -d \"$HOME/.config/" + clientName + "\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;")
+ }
}
+ scriptParts.push("echo \"$available_clients\"")
+
// Use a Process to check directory existence for all clients
- discordDetector.command = ["sh", "-c", "available_clients=\"\"; " + "for client in " + clientNames.join(" ") + "; do " + " if [ -d \"$HOME/.config/$client\" ]; then " + " available_clients=\"$available_clients $client\"; " + " fi; " + "done; " + "echo \"$available_clients\""]
+ discordDetector.command = ["sh", "-c", scriptParts.join(" ")]
discordDetector.running = true
}
@@ -64,12 +76,12 @@ Singleton {
}
}
- Logger.log("ProgramChecker", "Detected Discord clients:", detectedClients.join(", "))
+ Logger.i("ProgramChecker", "Detected Discord clients:", detectedClients.join(", "))
}
}
if (availableDiscordClients.length === 0) {
- Logger.log("ProgramChecker", "No Discord clients detected")
+ Logger.d("ProgramChecker", "No Discord clients detected")
}
}
@@ -85,6 +97,7 @@ Singleton {
"ghosttyAvailable": ["which", "ghostty"],
"footAvailable": ["which", "foot"],
"fuzzelAvailable": ["which", "fuzzel"],
+ "app2unitAvailable": ["which", "app2unit"],
"gpuScreenRecorderAvailable": ["sh", "-c", "command -v gpu-screen-recorder >/dev/null 2>&1 || (command -v flatpak >/dev/null 2>&1 && flatpak list --app | grep -q 'com.dec05eba.gpu_screen_recorder')"],
"wlsunsetAvailable": ["which", "wlsunset"]
})
@@ -159,7 +172,7 @@ Singleton {
// Function to check a specific program
function checkProgram(programProperty) {
if (!programsToCheck.hasOwnProperty(programProperty)) {
- Logger.warn("ProgramChecker", "Unknown program property:", programProperty)
+ Logger.w("ProgramChecker", "Unknown program property:", programProperty)
return
}
@@ -170,14 +183,14 @@ Singleton {
// Manual function to test Discord detection (for debugging)
function testDiscordDetection() {
- Logger.log("ProgramChecker", "Testing Discord detection...")
- Logger.log("ProgramChecker", "HOME:", Quickshell.env("HOME"))
+ Logger.d("ProgramChecker", "Testing Discord detection...")
+ Logger.d("ProgramChecker", "HOME:", Quickshell.env("HOME"))
// Test each client directory
for (var i = 0; i < MatugenTemplates.discordClients.length; i++) {
var client = MatugenTemplates.discordClients[i]
var configDir = client.configPath.replace("~", Quickshell.env("HOME"))
- Logger.log("ProgramChecker", "Checking:", configDir)
+ Logger.d("ProgramChecker", "Checking:", configDir)
}
detectDiscordClient()
diff --git a/Services/SwayService.qml b/Services/SwayService.qml
index 9e1ddd71..1ca2e638 100644
--- a/Services/SwayService.qml
+++ b/Services/SwayService.qml
@@ -43,9 +43,9 @@ Item {
queryDisplayScales()
})
initialized = true
- Logger.log("SwayService", "Initialized successfully")
+ Logger.i("SwayService", "Initialized successfully")
} catch (e) {
- Logger.error("SwayService", "Failed to initialize:", e)
+ Logger.e("SwayService", "Failed to initialize:", e)
}
}
@@ -70,7 +70,7 @@ Item {
onExited: function (exitCode) {
if (exitCode !== 0 || !accumulatedOutput) {
- Logger.error("SwayService", "Failed to query outputs, exit code:", exitCode)
+ Logger.e("SwayService", "Failed to query outputs, exit code:", exitCode)
accumulatedOutput = ""
return
}
@@ -101,7 +101,7 @@ Item {
CompositorService.onDisplayScalesUpdated(scales)
}
} catch (e) {
- Logger.error("SwayService", "Failed to parse outputs:", e)
+ Logger.e("SwayService", "Failed to parse outputs:", e)
} finally {
// Clear accumulated output for next query
accumulatedOutput = ""
@@ -147,7 +147,7 @@ Item {
workspaces.append(wsData)
}
} catch (e) {
- Logger.error("SwayService", "Error updating workspaces:", e)
+ Logger.e("SwayService", "Error updating workspaces:", e)
}
}
@@ -187,7 +187,7 @@ Item {
activeWindowChanged()
}
} catch (e) {
- Logger.error("SwayService", "Error updating windows:", e)
+ Logger.e("SwayService", "Error updating windows:", e)
}
}
@@ -198,7 +198,7 @@ Item {
try {
// Safely extract properties
- const appId = extractAppId(toplevel)
+ const appId = getAppId(toplevel)
const title = safeGetProperty(toplevel, "title", "")
const focused = toplevel.activated === true
@@ -213,8 +213,7 @@ Item {
}
}
- // Extract app ID from various possible sources
- function extractAppId(toplevel) {
+ function getAppId(toplevel) {
if (!toplevel)
return ""
@@ -272,7 +271,7 @@ Item {
try {
workspace.handle.activate()
} catch (e) {
- Logger.error("SwayService", "Failed to switch workspace:", e)
+ Logger.e("SwayService", "Failed to switch workspace:", e)
}
}
@@ -280,7 +279,7 @@ Item {
try {
window.handle.activate()
} catch (e) {
- Logger.error("SwayService", "Failed to switch window:", e)
+ Logger.e("SwayService", "Failed to switch window:", e)
}
}
@@ -288,7 +287,7 @@ Item {
try {
window.handle.close()
} catch (e) {
- Logger.error("SwayService", "Failed to close window:", e)
+ Logger.e("SwayService", "Failed to close window:", e)
}
}
@@ -296,7 +295,7 @@ Item {
try {
Quickshell.execDetached(["swaymsg", "exit"])
} catch (e) {
- Logger.error("SwayService", "Failed to logout:", e)
+ Logger.e("SwayService", "Failed to logout:", e)
}
}
}
diff --git a/Services/SystemStatService.qml b/Services/SystemStatService.qml
index 9692c1ee..1e89d5c2 100644
--- a/Services/SystemStatService.qml
+++ b/Services/SystemStatService.qml
@@ -42,7 +42,7 @@ Singleton {
// --------------------------------------------
Component.onCompleted: {
- Logger.log("SystemStat", "Service started with interval:", root.sleepDuration, "ms")
+ Logger.i("SystemStat", "Service started with interval:", root.sleepDuration, "ms")
// Kickoff the cpu name detection for temperature
cpuTempNameReader.checkNext()
@@ -121,11 +121,11 @@ Singleton {
function checkNext() {
if (currentIndex >= 16) {
// Check up to hwmon10
- Logger.warn("No supported temperature sensor found")
+ Logger.w("No supported temperature sensor found")
return
}
- //Logger.log("SystemStat", "---- Probing: hwmon", currentIndex)
+ //Logger.i("SystemStat", "---- Probing: hwmon", currentIndex)
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`
cpuTempNameReader.reload()
}
@@ -135,7 +135,7 @@ Singleton {
if (root.supportedTempCpuSensorNames.includes(name)) {
root.cpuTempSensorName = name
root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`
- Logger.log("SystemStat", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`)
+ Logger.i("SystemStat", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`)
} else {
currentIndex++
Qt.callLater(() => {
@@ -205,7 +205,7 @@ Singleton {
if (memTotal > 0) {
const usageKb = memTotal - memAvailable
- root.memGb = (usageKb / 1000000).toFixed(1)
+ root.memGb = (usageKb / 1048576).toFixed(1) // 1024*1024 = 1048576
root.memPercent = Math.round((usageKb / memTotal) * 100)
}
}
@@ -386,9 +386,9 @@ Singleton {
sum += root.intelTempValues[i]
}
root.cpuTemp = Math.round(sum / root.intelTempValues.length)
- //Logger.log("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`)
+ //Logger.i("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`)
} else {
- Logger.warn("SystemStat", "No temperature sensors found for coretemp")
+ Logger.w("SystemStat", "No temperature sensors found for coretemp")
root.cpuTemp = 0
}
return
diff --git a/Services/TooltipService.qml b/Services/TooltipService.qml
index f083848f..606e2c39 100644
--- a/Services/TooltipService.qml
+++ b/Services/TooltipService.qml
@@ -22,7 +22,7 @@ Singleton {
// Don't create if no text
if (!screen || !target || !text) {
- Logger.log("Tooltip", "No target or text")
+ Logger.i("Tooltip", "No target or text")
return
}
@@ -82,7 +82,7 @@ Singleton {
return newTooltip
} else {
- Logger.error("Tooltip", "Failed to create tooltip instance")
+ Logger.e("Tooltip", "Failed to create tooltip instance")
}
return null
diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml
index e1c864b5..41c5daf4 100644
--- a/Services/UpdateService.qml
+++ b/Services/UpdateService.qml
@@ -8,7 +8,7 @@ Singleton {
id: root
// Public properties
- property string baseVersion: "2.18.0"
+ property string baseVersion: "2.18.2"
property bool isDevelopment: true
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`
@@ -20,11 +20,11 @@ Singleton {
function checkForUpdates() {
// TODO: Implement update checking logic
- Logger.log("UpdateService", "Checking for updates...")
+ Logger.i("UpdateService", "Checking for updates...")
}
function init() {
// Ensure the singleton is created
- Logger.log("UpdateService", "Version:", root.currentVersion)
+ Logger.i("UpdateService", "Version:", root.currentVersion)
}
}
diff --git a/Services/WallpaperService.qml b/Services/WallpaperService.qml
index 2046f6cc..bed6647c 100644
--- a/Services/WallpaperService.qml
+++ b/Services/WallpaperService.qml
@@ -76,7 +76,7 @@ Singleton {
// -------------------------------------------------
function init() {
- Logger.log("Wallpaper", "Service started")
+ Logger.i("Wallpaper", "Service started")
translateModels()
@@ -247,11 +247,11 @@ Singleton {
}
if (screenName === undefined) {
- Logger.warn("Wallpaper", "setWallpaper", "no screen specified")
+ Logger.w("Wallpaper", "setWallpaper", "no screen specified")
return
}
- //Logger.log("Wallpaper", "setWallpaper on", screenName, ": ", path)
+ //Logger.i("Wallpaper", "setWallpaper on", screenName, ": ", path)
// Check if wallpaper actually changed
var oldPath = currentWallpapers[screenName] || ""
@@ -303,7 +303,7 @@ Singleton {
// -------------------------------------------------------------------
function setRandomWallpaper() {
- Logger.log("Wallpaper", "setRandomWallpaper")
+ Logger.d("Wallpaper", "setRandomWallpaper")
if (Settings.data.wallpaper.enableMultiMonitorDirectories) {
// Pick a random wallpaper per screen
@@ -331,7 +331,7 @@ Singleton {
// -------------------------------------------------------------------
function toggleRandomWallpaper() {
- Logger.log("Wallpaper", "toggleRandomWallpaper")
+ Logger.d("Wallpaper", "toggleRandomWallpaper")
if (Settings.data.wallpaper.randomEnabled) {
restartRandomWallpaperTimer()
setRandomWallpaper()
@@ -355,7 +355,7 @@ Singleton {
// -------------------------------------------------------------------
function refreshWallpapersList() {
- Logger.log("Wallpaper", "refreshWallpapersList")
+ Logger.d("Wallpaper", "refreshWallpapersList")
scanningCount = 0
// Force refresh by toggling the folder property on each FolderListModel
@@ -429,7 +429,7 @@ Singleton {
root.wallpaperLists[screenName] = files
scanningCount--
- Logger.log("Wallpaper", "List refreshed for", screenName, "count:", files.length)
+ Logger.d("Wallpaper", "List refreshed for", screenName, "count:", files.length)
root.wallpaperListChanged(screenName, files.length)
}
}
diff --git a/Widgets/NButton.qml b/Widgets/NButton.qml
index 654de952..9b97b5d2 100644
--- a/Widgets/NButton.qml
+++ b/Widgets/NButton.qml
@@ -25,6 +25,8 @@ Rectangle {
signal clicked
signal rightClicked
signal middleClicked
+ signal entered
+ signal exited
// Internal properties
property bool hovered: false
@@ -140,12 +142,14 @@ Rectangle {
onEntered: {
root.hovered = true
+ root.entered()
if (tooltipText) {
TooltipService.show(Screen, root, root.tooltipText)
}
}
onExited: {
root.hovered = false
+ root.exited()
if (tooltipText) {
TooltipService.hide()
}
diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml
index f27694e6..9fe63be9 100644
--- a/Widgets/NCheckbox.qml
+++ b/Widgets/NCheckbox.qml
@@ -61,7 +61,6 @@ RowLayout {
icon: "check"
color: root.activeOnColor
pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.5)
- font.weight: Style.fontWeightBold
}
MouseArea {
diff --git a/Widgets/NFilePicker.qml b/Widgets/NFilePicker.qml
index 4fd7875e..cc318f02 100644
--- a/Widgets/NFilePicker.qml
+++ b/Widgets/NFilePicker.qml
@@ -624,7 +624,6 @@ Popup {
NIcon {
icon: "filepicker-check"
pointSize: Style.fontSizeS
- font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
diff --git a/Widgets/NIcon.qml b/Widgets/NIcon.qml
index 87e63c1f..32e274f2 100644
--- a/Widgets/NIcon.qml
+++ b/Widgets/NIcon.qml
@@ -16,7 +16,7 @@ Text {
return ""
}
if (Icons.get(icon) === undefined) {
- Logger.warn("Icon", `"${icon}"`, "doesn't exist in the icons font")
+ Logger.w("Icon", `"${icon}"`, "doesn't exist in the icons font")
Logger.callStack()
return Icons.get(Icons.defaultIcon)
}
diff --git a/Widgets/NImageCached.qml b/Widgets/NImageCached.qml
index 1343828b..2645d479 100644
--- a/Widgets/NImageCached.qml
+++ b/Widgets/NImageCached.qml
@@ -23,7 +23,7 @@ Image {
onImagePathChanged: {
if (imagePath) {
imageHash = Checksum.sha256(imagePath)
- // Logger.log("NImageCached", imagePath, imageHash)
+ // Logger.i("NImageCached", imagePath, imageHash)
} else {
source = ""
imageHash = ""
@@ -31,8 +31,9 @@ Image {
}
onCachePathChanged: {
if (imageHash && cachePath) {
- // Try to load the cached version, failure will be detected below in onStatusChanged
- source = cachePath
+ // Check if cache file exists before trying to load it
+ cacheChecker.command = ["test", "-f", cachePath]
+ cacheChecker.running = true
}
}
onStatusChanged: {
@@ -48,4 +49,19 @@ Image {
})
}
}
+
+ // Check if cache file exists to avoid warnings
+ Process {
+ id: cacheChecker
+ running: false
+ onExited: function (exitCode) {
+ if (exitCode === 0 && root.cachePath) {
+ // Cache file exists, load it
+ root.source = root.cachePath
+ } else if (root.imagePath) {
+ // Cache doesn't exist, load original directly
+ root.source = root.imagePath
+ }
+ }
+ }
}
diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml
index 5651d8af..72e4905e 100644
--- a/Widgets/NPanel.qml
+++ b/Widgets/NPanel.qml
@@ -135,7 +135,7 @@ Loader {
readonly property real verticalBarWidth: Style.barHeight
Component.onCompleted: {
- Logger.log("NPanel", "Opened", root.objectName, "on", screen.name)
+ Logger.d("NPanel", "Opened", root.objectName, "on", screen.name)
dimmingOpacity = Style.opacityHeavy
}
@@ -148,7 +148,7 @@ Loader {
if (buttonName) {
setPosition()
}
- Logger.log("NPanel", "OnScreenChanged", root.screen.name)
+ Logger.d("NPanel", "OnScreenChanged", root.screen.name)
}
}
diff --git a/Widgets/NReorderCheckboxes.qml b/Widgets/NReorderCheckboxes.qml
index 9a8c1abc..a7d95927 100644
--- a/Widgets/NReorderCheckboxes.qml
+++ b/Widgets/NReorderCheckboxes.qml
@@ -217,7 +217,6 @@ Item {
icon: "check"
color: root.activeOnColor
pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.5)
- font.weight: Style.fontWeightBold
}
MouseArea {
diff --git a/Widgets/NSearchableComboBox.qml b/Widgets/NSearchableComboBox.qml
index ffb4f4cc..a9a578f4 100644
--- a/Widgets/NSearchableComboBox.qml
+++ b/Widgets/NSearchableComboBox.qml
@@ -233,7 +233,7 @@ RowLayout {
Layout.alignment: Qt.AlignRight
Repeater {
- model: badgeLocations
+ model: typeof badgeLocations !== 'undefined' ? badgeLocations : []
delegate: NBox {
width: Style.baseWidgetSize * 0.7
diff --git a/Widgets/NSectionEditor.qml b/Widgets/NSectionEditor.qml
index b1dbefcd..59b77d85 100644
--- a/Widgets/NSectionEditor.qml
+++ b/Widgets/NSectionEditor.qml
@@ -285,19 +285,19 @@ NBox {
if (dialog) {
dialog.open()
} else {
- Logger.error("NSectionEditor", "Failed to create settings dialog instance")
+ Logger.e("NSectionEditor", "Failed to create settings dialog instance")
}
}
if (component.status === Component.Ready) {
instantiateAndOpen()
} else if (component.status === Component.Error) {
- Logger.error("NSectionEditor", component.errorString())
+ Logger.e("NSectionEditor", component.errorString())
} else {
component.statusChanged.connect(function () {
if (component.status === Component.Ready) {
instantiateAndOpen()
} else if (component.status === Component.Error) {
- Logger.error("NSectionEditor", component.errorString())
+ Logger.e("NSectionEditor", component.errorString())
}
})
}
diff --git a/flake.nix b/flake.nix
index 54381f04..d2046bb1 100644
--- a/flake.nix
+++ b/flake.nix
@@ -49,9 +49,13 @@
nixpkgs.legacyPackages.${pkgs.system}.app2unit;
};
- nixosModules.default = {pkgs, ...}: {
+ nixosModules.default = {
+ pkgs,
+ lib,
+ ...
+ }: {
imports = [./nix/nixos-module.nix];
- services.noctalia-shell.package = self.packages.${pkgs.system}.default;
+ services.noctalia-shell.package = lib.mkDefault self.packages.${pkgs.system}.default;
};
};
}
diff --git a/lefthook.yml b/lefthook.yml
new file mode 100644
index 00000000..30dcc659
--- /dev/null
+++ b/lefthook.yml
@@ -0,0 +1,4 @@
+pre-commit:
+ jobs:
+ - name: format qml
+ run: cd "$(git rev-parse --show-toplevel)" && ./Bin/dev/qmlfmt.sh && git update-index --again
diff --git a/shell.qml b/shell.qml
index 39b76224..16227fd1 100644
--- a/shell.qml
+++ b/shell.qml
@@ -40,6 +40,7 @@ import qs.Modules.OSD
import qs.Modules.Settings
import qs.Modules.Toast
import qs.Modules.Wallpaper
+import qs.Modules.SetupWizard
ShellRoot {
id: shellRoot
@@ -48,8 +49,8 @@ ShellRoot {
property bool settingsLoaded: false
Component.onCompleted: {
- Logger.log("Shell", "---------------------------")
- Logger.log("Shell", "Noctalia Hello!")
+ Logger.i("Shell", "---------------------------")
+ Logger.i("Shell", "Noctalia Hello!")
}
Connections {
@@ -78,17 +79,19 @@ ShellRoot {
sourceComponent: Item {
Component.onCompleted: {
- Logger.log("Shell", "---------------------------")
+ Logger.i("Shell", "---------------------------")
WallpaperService.init()
AppThemeService.init()
ColorSchemeService.init()
BarWidgetRegistry.init()
LocationService.init()
NightLightService.apply()
+ DarkModeService.init()
FontService.init()
HooksService.init()
BluetoothService.init()
BatteryService.init()
+ IdleInhibitorService.init()
}
Background {}
@@ -168,4 +171,48 @@ ShellRoot {
}
}
}
+
+ // ------------------------------
+ // Setup Wizard
+ Loader {
+ id: setupWizardLoader
+ active: false
+ asynchronous: true
+ sourceComponent: SetupWizard {}
+ onLoaded: {
+ if (setupWizardLoader.item && setupWizardLoader.item.open) {
+ setupWizardLoader.item.open()
+ }
+ }
+ }
+
+ Connections {
+ target: Settings
+ function onSettingsLoaded() {
+ // Only open the setup wizard for new users
+ if (!Settings.data.setupCompleted) {
+ checkSetupWizard()
+ }
+ }
+ }
+
+ function checkSetupWizard() {
+ // Wait for distro service
+ if (!DistroService.isReady) {
+ Qt.callLater(checkSetupWizard)
+ return
+ }
+
+ // No setup wizard on NixOS
+ if (DistroService.isNixOS) {
+ Settings.data.setupCompleted = true
+ return
+ }
+
+ if (Settings.data.settingsVersion >= Settings.settingsVersion) {
+ setupWizardLoader.active = true
+ } else {
+ Settings.data.setupCompleted = true
+ }
+ }
}