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 + } + } }