diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index ea5fe9ef..c1b62f3b 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -18,6 +18,7 @@ jobs: - name: Check QML format shell: fish {0} run: | + /usr/lib/qt6/bin/qmlformat --version for file in (string match -v 'build/*' **.qml) /usr/lib/qt6/bin/qmlformat $file | diff -u $file - || exit 1 end @@ -34,5 +35,6 @@ jobs: - name: Check C++ format shell: fish {0} run: | + clang-format --version find plugin extras -name '*.cpp' -o -name '*.hpp' \ | xargs clang-format --dry-run --Werror diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7962963c..9dd0d576 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,6 +25,8 @@ jobs: - name: Lint QML shell: fish {0} run: | + /usr/lib/qt6/bin/qmllint --version + # Generate tooling touch .qmlls.ini QT_QPA_PLATFORM=offscreen QML2_IMPORT_PATH="$PWD/build/qml:$QML2_IMPORT_PATH" timeout 2 qs -p . diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml index 74fe5435..34b9a6fc 100644 --- a/.github/workflows/update-image.yml +++ b/.github/workflows/update-image.yml @@ -23,7 +23,13 @@ jobs: run: | cat > /tmp/Dockerfile < /etc/pacman.d/mirrorlist && \ + pacman -Syyuu --needed --noconfirm sudo base-devel cmake ninja fish git clazy qt6-declarative qt6-shadertools python libpipewire aubio libqalculate && \ useradd -m builder && \ echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ diff --git a/CMakeLists.txt b/CMakeLists.txt index b23f7b48..980065b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,3 +71,20 @@ if("shell" IN_LIST ENABLE_MODULES) install(FILES LICENSE DESTINATION "${INSTALL_QSCONFDIR}") endif() + +if("plugin" IN_LIST ENABLE_MODULES OR "shell" IN_LIST ENABLE_MODULES) + message(STATUS "Fetching M3Shapes module") + include(FetchContent) + FetchContent_Declare( + m3shapes_external + GIT_REPOSITORY https://github.com/soramanew/m3shapes.git + GIT_TAG main + ) + FetchContent_MakeAvailable(m3shapes_external) + message(STATUS "Done fetching M3Shapes module") + + # Fix m3shapes wrong rpath + if(TARGET m3shapesplugin) + set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN") + endif() +endif() diff --git a/README.md b/README.md index 13b5ec8a..1e72e690 100644 --- a/README.md +++ b/README.md @@ -240,12 +240,14 @@ For example, to disable the bar on DP-1: > - `launcher` (`specialPrefix`, `actionPrefix`, `enableDangerousActions`, `vimKeybinds`, > `favouriteApps`, `hiddenApps`, `actions`) > - `launcher.useFuzzy` (`apps`, `actions`, `schemes`, `variants`, `wallpapers`) -> - `notifs` (`expire`, `fullscreen`, `defaultExpireTimeout`, `actionOnClick`) +> - `notifs` (`expire`, `fullscreen`, `defaultExpireTimeout`, `fullscreenExpireTimeout`, `actionOnClick`) > - `lock` (`enableFprint`, `maxFprintTries`) -> - `utilities` (`toasts`, `vpn`) +> - `nexus` (`networkRescanInterval`) +> - `utilities.toasts` (all except `fullscreen`) +> - `utilities.vpn` (`enabled`, `provider`) > - `services` (`weatherLocation`, `useFahrenheit`, `useFahrenheitPerformance`, `useTwelveHourClock`, > `gpuType`, `visualiserBars`, `audioIncrement`, `brightnessIncrement`, `maxVolume`, `smartScheme`, -> `defaultPlayer`, `playerAliases`, `showLyrics`, `lyricsBackend`) +> `defaultPlayer`, `playerAliases`, `lyricsBackend`) > - `paths` (`wallpaperDir`, `lyricsDir`) > > @@ -265,31 +267,61 @@ For example, to disable the bar on DP-1: "enabled": true, "appearance": { "deformScale": 1, - "anim": { - "durations": { - "scale": 1 - } - }, - "font": { - "family": { - "clock": "Rubik", - "material": "Material Symbols Rounded", - "mono": "CaskaydiaCove NF", - "sans": "Rubik" - }, - "size": { - "scale": 1 - } - }, - "padding": { - "scale": 1 - }, "rounding": { "scale": 1 }, "spacing": { "scale": 1 }, + "padding": { + "scale": 1 + }, + "font": { + "scale": 1, + "clock": "Rubik", + "headline": { + "family": "GoogleSansFlex", + "large": { "size": 32, "weight": 500, "italic": false, "vaxes": { "ROND": 25 } }, + "medium": { "size": 28, "weight": 500, "italic": false, "vaxes": { "ROND": 25 } }, + "small": { "size": 24, "weight": 500, "italic": false, "vaxes": { "ROND": 25 } } + }, + "title": { + "family": "GoogleSansFlex", + "large": { "size": 22, "weight": 500, "italic": false, "vaxes": { "ROND": 25 } }, + "medium": { "size": 16, "weight": 500, "italic": false, "vaxes": { "ROND": 25 } }, + "small": { "size": 14, "weight": 500, "italic": false, "vaxes": { "ROND": 25 } } + }, + "body": { + "family": "GoogleSansFlex", + "large": { "size": 16, "weight": 400, "italic": false, "vaxes": { "ROND": 25 } }, + "medium": { "size": 14, "weight": 400, "italic": false, "vaxes": { "ROND": 25 } }, + "small": { "size": 12, "weight": 400, "italic": false, "vaxes": { "ROND": 25 } } + }, + "label": { + "family": "GoogleSansFlex", + "large": { "size": 14, "weight": 500, "italic": false, "vaxes": { "ROND": 25 } }, + "medium": { "size": 12, "weight": 500, "italic": false, "vaxes": { "ROND": 25 } }, + "small": { "size": 11, "weight": 400, "italic": false, "vaxes": { "ROND": 25 } } + }, + "mono": { + "family": "CaskaydiaCove NF", + "large": { "size": 16, "weight": 400, "italic": false, "vaxes": {} }, + "medium": { "size": 14, "weight": 400, "italic": false, "vaxes": {} }, + "small": { "size": 12, "weight": 400, "italic": false, "vaxes": {} } + }, + "icon": { + "family": "Material Symbols Rounded", + "extraLarge": { "size": 36, "weight": 400, "italic": false, "vaxes": {} }, + "large": { "size": 24, "weight": 400, "italic": false, "vaxes": {} }, + "medium": { "size": 18, "weight": 400, "italic": false, "vaxes": {} }, + "small": { "size": 15, "weight": 400, "italic": false, "vaxes": {} } + } + }, + "anim": { + "durations": { + "scale": 1 + } + }, "transparency": { "enabled": false, "base": 0.85, @@ -297,7 +329,7 @@ For example, to disable the bar on DP-1: } }, "general": { - "logo": "caelestia", + "logo": "", "showOverFullscreen": false, "mediaGifSpeedAdjustment": 300, "sessionGifSpeed": 0.7, @@ -307,6 +339,25 @@ For example, to disable the bar on DP-1: "playback": ["mpv"], "explorer": ["thunar"] }, + "idle": { + "lockBeforeSleep": true, + "inhibitWhenAudio": true, + "timeouts": [ + { + "timeout": 180, + "idleAction": "lock" + }, + { + "timeout": 300, + "idleAction": "dpms off", + "returnAction": "dpms on" + }, + { + "timeout": 600, + "idleAction": ["systemctl", "suspend-then-hibernate"] + } + ] + }, "battery": { "warnLevels": [ { @@ -330,65 +381,102 @@ For example, to disable the bar on DP-1: } ], "criticalLevel": 3 - }, - "idle": { - "lockBeforeSleep": true, - "inhibitWhenAudio": true, - "timeouts": [ - { - "timeout": 180, - "idleAction": "lock" - }, - { - "timeout": 300, - "idleAction": "dpms off", - "returnAction": "dpms on" - }, - { - "timeout": 600, - "idleAction": ["systemctl", "suspend-then-hibernate"] - } - ] } }, "background": { + "enabled": true, + "wallpaperEnabled": true, "desktopClock": { "enabled": false, "scale": 1.0, "position": "bottom-right", - "shadow": { - "enabled": true, - "opacity": 0.7, - "blur": 0.4 - }, + "invertColors": false, "background": { "enabled": false, "opacity": 0.7, "blur": true }, - "invertColors": false + "shadow": { + "enabled": true, + "opacity": 0.7, + "blur": 0.4 + } }, - "enabled": true, "visualiser": { - "blur": false, "enabled": false, "autoHide": true, + "blur": false, "rounding": 1, "spacing": 1 } }, "bar": { + "persistent": true, + "showOnHover": true, + "dragThreshold": 20, + "scrollActions": { + "workspaces": true, + "volume": true, + "brightness": true + }, + "popouts": { + "activeWindow": true, + "tray": true, + "statusIcons": true + }, + "workspaces": { + "shown": 5, + "activeIndicator": true, + "occupiedBg": false, + "showWindows": true, + "showWindowsOnSpecialWorkspaces": true, + "maxWindowIcons": 5, + "activeTrail": false, + "perMonitorWorkspaces": true, + "label": " ", + "occupiedLabel": "󰮯", + "activeLabel": "󰮯", + "capitalisation": "preserve", + "specialWorkspaceIcons": [ + { + "name": "steam", + "icon": "sports_esports" + } + ], + "windowIcons": [ + { + "regex": "steam(_app_(default|[0-9]+))?", + "icon": "sports_esports" + } + ] + }, "activeWindow": { "compact": false, "inverted": false, "showOnHover": true }, + "tray": { + "background": false, + "recolour": false, + "compact": false, + "iconSubs": [], + "hiddenIcons": [] + }, + "status": { + "showAudio": false, + "showMicrophone": false, + "showKbLayout": false, + "showNetwork": true, + "showWifi": true, + "showBluetooth": true, + "showBattery": true, + "showLockStatus": true + }, "clock": { "background": false, "showDate": false, "showIcon": true }, - "dragThreshold": 20, "entries": [ { "id": "logo", @@ -427,66 +515,12 @@ For example, to disable the bar on DP-1: "enabled": true } ], - "persistent": true, - "popouts": { - "activeWindow": true, - "statusIcons": true, - "tray": true - }, - "scrollActions": { - "brightness": true, - "workspaces": true, - "volume": true - }, - "showOnHover": true, - "status": { - "showAudio": false, - "showBattery": true, - "showBluetooth": true, - "showKbLayout": false, - "showMicrophone": false, - "showNetwork": true, - "showWifi": true, - "showLockStatus": true - }, - "tray": { - "background": false, - "compact": false, - "iconSubs": [], - "recolour": false - }, - "workspaces": { - "activeIndicator": true, - "activeLabel": "󰮯", - "activeTrail": false, - "label": " ", - "occupiedBg": false, - "occupiedLabel": "󰮯", - "perMonitorWorkspaces": true, - "showWindows": true, - "shown": 5, - "specialWorkspaceIcons": [ - { - "name": "steam", - "icon": "sports_esports" - } - ], - "windowIcons": [ - { - "regex": "steam(_app_(default|[0-9]+))?", - "icon": "sports_esports" - } - ] - }, - "excludedScreens": [""], - "activeWindow": { - "inverted": false - } + "excludedScreens": [] }, "border": { + "thickness": 10, "rounding": 25, - "smoothing": 32, - "thickness": 10 + "smoothing": 20 }, "dashboard": { "enabled": true, @@ -495,11 +529,37 @@ For example, to disable the bar on DP-1: "showMedia": true, "showPerformance": true, "showWeather": true, + "mediaUpdateInterval": 500, + "resourceUpdateInterval": 1000, "dragThreshold": 50, - "mediaUpdateInterval": 500 + "performance": { + "showBattery": true, + "showGpu": true, + "showCpu": true, + "showMemory": true, + "showStorage": true, + "showNetwork": true + } }, "launcher": { + "enabled": true, + "showOnHover": false, + "maxShown": 7, + "maxWallpapers": 9, + "specialPrefix": "@", "actionPrefix": ">", + "enableDangerousActions": false, + "dragThreshold": 50, + "vimKeybinds": false, + "favouriteApps": [], + "hiddenApps": [], + "useFuzzy": { + "apps": false, + "actions": false, + "schemes": false, + "variants": false, + "wallpapers": false + }, "actions": [ { "name": "Calculator", @@ -533,14 +593,6 @@ For example, to disable the bar on DP-1: "enabled": true, "dangerous": false }, - { - "name": "Transparency", - "icon": "opacity", - "description": "Change shell transparency", - "command": ["autocomplete", "transparency"], - "enabled": false, - "dangerous": false - }, { "name": "Random", "icon": "casino", @@ -609,71 +661,57 @@ For example, to disable the bar on DP-1: "name": "Settings", "icon": "settings", "description": "Configure the shell", - "command": ["caelestia", "shell", "controlCenter", "open"], + "command": ["caelestia", "shell", "nexus", "open"], "enabled": true, "dangerous": false } - ], - "dragThreshold": 50, - "vimKeybinds": false, - "enableDangerousActions": false, - "maxShown": 7, - "maxWallpapers": 9, - "specialPrefix": "@", - "useFuzzy": { - "apps": false, - "actions": false, - "schemes": false, - "variants": false, - "wallpapers": false - }, - "showOnHover": false, - "favouriteApps": [], - "hiddenApps": [] + ] }, "lock": { - "recolourLogo": false, + "recolourLogo": true, + "enableFprint": true, + "maxFprintTries": 3, "hideNotifs": false }, + "nexus": { + "wallpapersPerRow": 4, + "networkRescanInterval": 15000 + }, "notifs": { - "actionOnClick": false, - "clearThreshold": 0.3, + "expire": true, + "fullscreen": "on", "defaultExpireTimeout": 5000, + "fullscreenExpireTimeout": 2000, + "clearThreshold": 0.3, "expandThreshold": 20, - "openExpanded": false, - "expire": false + "actionOnClick": false, + "groupPreviewNum": 3, + "openExpanded": false }, "osd": { "enabled": true, + "hideDelay": 2000, "enableBrightness": true, - "enableMicrophone": false, - "hideDelay": 2000 - }, - "paths": { - "mediaGif": "root:/assets/bongocat.gif", - "sessionGif": "root:/assets/kurukuru.gif", - "noNotifsPic": "root:/assets/dino.png", - "lockNoNotifsPic": "root:/assets/dino.png", - "wallpaperDir": "~/Pictures/Wallpapers", - "lyricsDir": "~/Music/lyrics" + "enableMicrophone": false }, "services": { - "audioIncrement": 0.1, - "brightnessIncrement": 0.1, - "maxVolume": 1.0, - "defaultPlayer": "Spotify", - "gpuType": "", - "playerAliases": [{ "from": "com.github.th_ch.youtube_music", "to": "YT Music" }], "weatherLocation": "", "useFahrenheit": false, "useFahrenheitPerformance": false, "useTwelveHourClock": false, + "gpuType": "", + "visualiserBars": 60, + "audioIncrement": 0.1, + "brightnessIncrement": 0.1, + "maxVolume": 1.0, "smartScheme": true, - "visualiserBars": 45 + "defaultPlayer": "Spotify", + "playerAliases": [{ "from": "com.github.th_ch.youtube_music", "to": "YT Music" }], + "lyricsBackend": "Auto" }, "session": { - "dragThreshold": 30, "enabled": true, + "dragThreshold": 30, "vimKeybinds": false, "icons": { "logout": "logout", @@ -689,28 +727,29 @@ For example, to disable the bar on DP-1: } }, "sidebar": { - "dragThreshold": 80, - "enabled": true + "enabled": true, + "dragThreshold": 80 }, "utilities": { "enabled": true, "maxToasts": 4, "toasts": { - "audioInputChanged": true, - "audioOutputChanged": true, - "capsLockChanged": true, - "chargingChanged": true, + "fullscreen": "off", "configLoaded": true, - "dndChanged": true, + "chargingChanged": true, "gameModeChanged": true, + "dndChanged": true, + "audioOutputChanged": true, + "audioInputChanged": true, + "capsLockChanged": true, + "numLockChanged": true, "kbLayoutChanged": true, "kbLimit": true, - "numLockChanged": true, "vpnChanged": true, "nowPlaying": false }, "vpn": { - "enabled": true, + "enabled": false, "provider": [ { "name": "wireguard", @@ -734,8 +773,8 @@ For example, to disable the bar on DP-1: "enabled": true }, { - "enabled": true, - "id": "settings" + "id": "settings", + "enabled": true }, { "id": "gameMode", @@ -747,9 +786,17 @@ For example, to disable the bar on DP-1: }, { "id": "vpn", - "enabled": true + "enabled": false } ] + }, + "paths": { + "wallpaperDir": "~/Pictures/Wallpapers", + "lyricsDir": "~/Music/lyrics/", + "sessionGif": "root:/assets/kurukuru.gif", + "mediaGif": "root:/assets/bongocat.gif", + "noNotifsPic": "root:/assets/dino.png", + "lockNoNotifsPic": "root:/assets/dino.png" } } ``` diff --git a/assets/google-sans-flex/GoogleSansFlex-VariableFont_GRAD,ROND,opsz,slnt,wdth,wght.ttf b/assets/google-sans-flex/GoogleSansFlex-VariableFont_GRAD,ROND,opsz,slnt,wdth,wght.ttf new file mode 100644 index 00000000..83f272d2 Binary files /dev/null and b/assets/google-sans-flex/GoogleSansFlex-VariableFont_GRAD,ROND,opsz,slnt,wdth,wght.ttf differ diff --git a/assets/google-sans-flex/LICENSE b/assets/google-sans-flex/LICENSE new file mode 100644 index 00000000..e1fa6e8f --- /dev/null +++ b/assets/google-sans-flex/LICENSE @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/shaders/fade.frag b/assets/shaders/fade.frag deleted file mode 100644 index a6cdf70d..00000000 --- a/assets/shaders/fade.frag +++ /dev/null @@ -1,26 +0,0 @@ -#version 440 - -layout(location = 0) in vec2 qt_TexCoord0; -layout(location = 0) out vec4 fragColor; - -layout(std140, binding = 0) uniform buf { - mat4 qt_Matrix; - float qt_Opacity; - float fadeMargin; -}; - -layout(binding = 1) uniform sampler2D source; - -void main() { - vec4 tex = texture(source, qt_TexCoord0); - float factor = 1.0; - float margin = 0.1; - - if (qt_TexCoord0.y < margin) { - factor = qt_TexCoord0.y / margin; - } else if (qt_TexCoord0.y > (1.0 - margin)) { - factor = (1.0 - qt_TexCoord0.y) / margin; - } - - fragColor = tex * factor * qt_Opacity; -} diff --git a/assets/shaders/fade.frag.qsb b/assets/shaders/fade.frag.qsb deleted file mode 100644 index 888e4c10..00000000 Binary files a/assets/shaders/fade.frag.qsb and /dev/null differ diff --git a/assets/shaders/opacitymask.frag b/assets/shaders/opacitymask.frag deleted file mode 100644 index 94a80b8a..00000000 --- a/assets/shaders/opacitymask.frag +++ /dev/null @@ -1,19 +0,0 @@ -#version 440 - -layout(location = 0) in vec2 qt_TexCoord0; -layout(location = 0) out vec4 fragColor; - -layout(std140, binding = 0) uniform buf { - // qt_Matrix and qt_Opacity must always be both present - // if the built-in vertex shader is used. - mat4 qt_Matrix; - float qt_Opacity; -}; - -layout(binding = 1) uniform sampler2D source; -layout(binding = 2) uniform sampler2D maskSource; - -void main() -{ - fragColor = texture(source, qt_TexCoord0.st) * (texture(maskSource, qt_TexCoord0.st).a) * qt_Opacity; -} diff --git a/assets/shaders/opacitymask.frag.qsb b/assets/shaders/opacitymask.frag.qsb deleted file mode 100644 index 7bf97c28..00000000 Binary files a/assets/shaders/opacitymask.frag.qsb and /dev/null differ diff --git a/assets/wallpaper.webp b/assets/wallpaper.webp new file mode 100644 index 00000000..ef273bbe Binary files /dev/null and b/assets/wallpaper.webp differ diff --git a/components/Anim.qml b/components/Anim.qml index 8bc4468a..16f8bdb5 100644 --- a/components/Anim.qml +++ b/components/Anim.qml @@ -13,33 +13,48 @@ NumberAnimation { EmphasizedExtraLarge, FastSpatial, DefaultSpatial, - SlowSpatial + SlowSpatial, + FastEffects, + DefaultEffects, + SlowEffects } - property int type: Anim.Standard + property int type: Anim.DefaultSpatial duration: { - if (type < Anim.StandardSmall || type > Anim.SlowSpatial) + if (type < Anim.StandardSmall || type > Anim.SlowEffects) return Tokens.anim.durations.normal; - if (type == Anim.FastSpatial) + if (type === Anim.FastSpatial) return Tokens.anim.durations.expressiveFastSpatial; - if (type == Anim.DefaultSpatial) + if (type === Anim.DefaultSpatial) return Tokens.anim.durations.expressiveDefaultSpatial; - if (type == Anim.SlowSpatial) + if (type === Anim.SlowSpatial) return Tokens.anim.durations.expressiveSlowSpatial; + if (type === Anim.FastEffects) + return Tokens.anim.durations.expressiveFastEffects; + if (type === Anim.DefaultEffects) + return Tokens.anim.durations.expressiveDefaultEffects; + if (type === Anim.SlowEffects) + return Tokens.anim.durations.expressiveSlowEffects; const types = ["small", "normal", "large", "extraLarge"]; const idx = type % 4; // 0-7 are the 4 standard types return Tokens.anim.durations[types[idx]]; } easing: { - if (type == Anim.FastSpatial) + if (type === Anim.FastSpatial) return Tokens.anim.expressiveFastSpatial; - if (type == Anim.DefaultSpatial) + if (type === Anim.DefaultSpatial) return Tokens.anim.expressiveDefaultSpatial; - if (type == Anim.SlowSpatial) + if (type === Anim.SlowSpatial) return Tokens.anim.expressiveSlowSpatial; + if (type === Anim.FastEffects) + return Tokens.anim.expressiveFastEffects; + if (type === Anim.DefaultEffects) + return Tokens.anim.expressiveDefaultEffects; + if (type === Anim.SlowEffects) + return Tokens.anim.expressiveSlowEffects; if (type >= Anim.EmphasizedSmall && type <= Anim.EmphasizedExtraLarge) return Tokens.anim.emphasized; diff --git a/components/AnimLoader.qml b/components/AnimLoader.qml new file mode 100644 index 00000000..a3ef4ddc --- /dev/null +++ b/components/AnimLoader.qml @@ -0,0 +1,42 @@ +import QtQuick + +Loader { + id: root + + property Component sourceComp + property bool isComplete + property int outAnimType: Anim.FastEffects + property int inAnimType: Anim.DefaultEffects + + asynchronous: true + Component.onCompleted: { + isComplete = true; + sourceComponent = sourceComp; + } + onSourceCompChanged: { + if (isComplete) + anim.restart(); + } + + SequentialAnimation { + id: anim + + running: false + + Anim { + target: root + property: "opacity" + to: 0 + type: root.outAnimType + } + ScriptAction { + script: root.sourceComponent = root.sourceComp + } + Anim { + target: root + property: "opacity" + to: 1 + type: root.inAnimType + } + } +} diff --git a/components/CAnim.qml b/components/CAnim.qml index b86fcce1..17168de3 100644 --- a/components/CAnim.qml +++ b/components/CAnim.qml @@ -2,6 +2,6 @@ import QtQuick import Caelestia.Config ColorAnimation { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standard + duration: Tokens.anim.durations.expressiveSlowEffects + easing: Tokens.anim.expressiveSlowEffects } diff --git a/components/ConnectionHeader.qml b/components/ConnectionHeader.qml index 691fd3c5..ff4061b5 100644 --- a/components/ConnectionHeader.qml +++ b/components/ConnectionHeader.qml @@ -9,22 +9,20 @@ ColumnLayout { required property string icon required property string title - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Layout.alignment: Qt.AlignHCenter MaterialIcon { Layout.alignment: Qt.AlignHCenter animate: true text: root.icon - font.pointSize: Tokens.font.size.extraLarge * 3 - font.bold: true + fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).weight(Font.Bold).build() } StyledText { Layout.alignment: Qt.AlignHCenter animate: true text: root.title - font.pointSize: Tokens.font.size.large - font.bold: true + font: Tokens.font.title.builders.medium.weight(Font.Bold).build() } } diff --git a/components/ConnectionInfoSection.qml b/components/ConnectionInfoSection.qml index d94c3a6e..957dbb0f 100644 --- a/components/ConnectionInfoSection.qml +++ b/components/ConnectionInfoSection.qml @@ -9,7 +9,7 @@ ColumnLayout { required property var deviceDetails - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { text: qsTr("IP Address") @@ -18,40 +18,40 @@ ColumnLayout { StyledText { text: root.deviceDetails?.ipAddress || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("Subnet Mask") } StyledText { text: root.deviceDetails?.subnet || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("Gateway") } StyledText { text: root.deviceDetails?.gateway || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("DNS Servers") } StyledText { text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(", ") : qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small wrapMode: Text.Wrap Layout.maximumWidth: parent.width } diff --git a/components/MaterialIcon.qml b/components/MaterialIcon.qml index 739c50ba..513244ad 100644 --- a/components/MaterialIcon.qml +++ b/components/MaterialIcon.qml @@ -1,16 +1,11 @@ +import QtQuick import Caelestia.Config import qs.services StyledText { property real fill property int grade: Colours.light ? 0 : -25 + property font fontStyle: Tokens.font.icon.small - font.family: Tokens.font.family.material - font.pointSize: Tokens.font.size.larger - font.variableAxes: ({ - FILL: fill.toFixed(1), - GRAD: grade, - opsz: fontInfo.pixelSize, - wght: fontInfo.weight - }) + font: Tokens.font.icon.size(fontStyle.pointSize).weight(fontStyle.weight).vaxes(fontStyle.variableAxes).fill(fill.toFixed(1)).grade(grade).build() } diff --git a/components/PropertyRow.qml b/components/PropertyRow.qml index 4d302509..980a7d9d 100644 --- a/components/PropertyRow.qml +++ b/components/PropertyRow.qml @@ -11,16 +11,16 @@ ColumnLayout { required property string value property bool showTopMargin: false - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { - Layout.topMargin: root.showTopMargin ? Tokens.spacing.normal : 0 + Layout.topMargin: root.showTopMargin ? Tokens.spacing.medium : 0 text: root.label } StyledText { text: root.value color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } } diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml index 7aa3f13f..f539fa77 100644 --- a/components/SectionContainer.qml +++ b/components/SectionContainer.qml @@ -8,13 +8,13 @@ StyledRect { id: root default property alias content: contentColumn.data - property real contentSpacing: Tokens.spacing.larger + property real contentSpacing: Tokens.spacing.large property bool alignTop: false Layout.fillWidth: true - implicitHeight: contentColumn.implicitHeight + Tokens.padding.large * 2 + implicitHeight: contentColumn.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : Colours.palette.m3surfaceContainerHigh ColumnLayout { diff --git a/components/SectionHeader.qml b/components/SectionHeader.qml index 09364143..23eb913c 100644 --- a/components/SectionHeader.qml +++ b/components/SectionHeader.qml @@ -13,10 +13,9 @@ ColumnLayout { spacing: 0 StyledText { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased text: root.title - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() } StyledText { diff --git a/components/StateLayer.qml b/components/StateLayer.qml index 2ce8c500..d30ab731 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Shapes +import Caelestia import Caelestia.Config import qs.services @@ -8,10 +9,11 @@ MouseArea { property bool disabled property bool showHoverBackground: true + property bool manualPressOverride readonly property alias rect: base property bool shapeMorph - property real stateOpacity: pressed ? 0.1 : containsMouse ? 0.08 : 0 + property real stateOpacity: containsMouse ? 0.08 : 0 property real pressX: width / 2 property real pressY: height / 2 @@ -29,7 +31,7 @@ MouseArea { const d2 = distSq(width, 0); const d3 = distSq(0, height); const d4 = distSq(width, height); - return Math.sqrt(Math.max(d1, d2, d3, d4)) * (shapeMorph ? 1.16 : 1); + return (Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0)) * 1.3; } property real endRadiusAtPress @@ -59,12 +61,17 @@ MouseArea { onPressed: e => press(e.x, e.y) onPressedChanged: { - if (!pressed && !rippleAnim.running && circle.opacity > 0) + if (!(pressed || manualPressOverride) && !rippleAnim.running && circle.opacity > 0) + fadeAnim.start(); + } + + onManualPressOverrideChanged: { + if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) fadeAnim.start(); } onCircleRadiusChanged: { - if (!pressed && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) + if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) fadeAnim.start(); } @@ -75,7 +82,7 @@ MouseArea { target: root property: "circleRadius" to: root.endRadius - easing: Tokens.anim.expressiveSlowEffects + easing: Tokens.anim.standard duration: Tokens.anim.durations.expressiveSlowEffects * 2 } @@ -85,8 +92,7 @@ MouseArea { target: circle property: "opacity" to: 0 - easing: Tokens.anim.expressiveSlowEffects - duration: Tokens.anim.durations.expressiveSlowEffects + type: Anim.SlowEffects } StyledRect { @@ -95,8 +101,14 @@ MouseArea { anchors.fill: parent opacity: root.stateOpacity color: Colours.palette.m3onSurface - // Pick up radius from parent if it has one (parent can be anything with a radius property) - radius: root.parent?.radius ?? 0 // qmllint disable missing-property + // Pick up radius from parent if it has one (parent can be anything with radius props) + // qmllint disable missing-property + radius: root.parent?.radius ?? 0 + topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0 + topRightRadius: root.parent?.topRightRadius ?? radius ?? 0 + bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0 + bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0 + // qmllint enable missing-property } Shape { @@ -122,12 +134,12 @@ MouseArea { color: Qt.alpha(base.color, 1) } GradientStop { - position: 0.99 + position: CUtils.clamp(1 - 0.2 * root.endRadius / root.circleRadius, 0.01, 0.99) color: Qt.alpha(base.color, 1) } GradientStop { position: 1 - color: Qt.alpha(base.color, 0) + color: Qt.alpha(base.color, CUtils.clamp((root.circleRadius / root.endRadius - 0.9) / 0.1, 0, 1)) } } @@ -135,14 +147,14 @@ MouseArea { startY: 0 PathLine { - x: root.width - root.clamp(base.topLeftRadius) + x: root.width - root.clamp(base.topRightRadius) y: 0 } PathArc { - relativeX: root.clamp(base.topLeftRadius) - relativeY: root.clamp(base.topLeftRadius) - radiusX: root.clamp(base.topLeftRadius) - radiusY: root.clamp(base.topLeftRadius) + relativeX: root.clamp(base.topRightRadius) + relativeY: root.clamp(base.topRightRadius) + radiusX: root.clamp(base.topRightRadius) + radiusY: root.clamp(base.topRightRadius) } PathLine { x: root.width @@ -179,8 +191,7 @@ MouseArea { Behavior on stateOpacity { Anim { - easing: Tokens.anim.expressiveDefaultEffects - duration: Tokens.anim.durations.expressiveDefaultEffects + type: Anim.DefaultEffects } } } diff --git a/components/StyledText.qml b/components/StyledText.qml index d3624c58..e39e72e7 100644 --- a/components/StyledText.qml +++ b/components/StyledText.qml @@ -8,16 +8,11 @@ Text { id: root property bool animate: false - property string animateProp: "scale" - property real animateFrom: 0 - property real animateTo: 1 - property int animateDuration: Tokens.anim.durations.normal renderType: Text.NativeRendering textFormat: Text.PlainText color: Colours.palette.m3onSurface - font.family: Tokens.font.family.sans - font.pointSize: Tokens.font.size.smaller + font: Tokens.font.body.small Behavior on color { CAnim {} @@ -28,20 +23,18 @@ Text { SequentialAnimation { Anim { - to: root.animateFrom - easing: Tokens.anim.standardAccel + target: root + property: "opacity" + to: 0 + type: Anim.FastEffects } PropertyAction {} Anim { - to: root.animateTo - easing: Tokens.anim.standardDecel + target: root + property: "opacity" + to: 1 + type: Anim.DefaultEffects } } } - - component Anim: NumberAnimation { - target: root - property: root.animateProp - duration: root.animateDuration / 2 - } } diff --git a/components/containers/StyledFlickable.qml b/components/containers/StyledFlickable.qml index b792e583..5411c4e1 100644 --- a/components/containers/StyledFlickable.qml +++ b/components/containers/StyledFlickable.qml @@ -4,11 +4,28 @@ import qs.components Flickable { id: root + property bool doneFakeFlick + maximumFlickVelocity: 3000 rebound: Transition { + onRunningChanged: { + if (!running && !root.doneFakeFlick) { + root.doneFakeFlick = true; + root.flick(1, 1); + root.flick(-1, -1); + Qt.callLater(() => root.cancelFlick()); + } + } + Anim { properties: "x,y" } } + + Timer { + running: root.doneFakeFlick + interval: 10 + onTriggered: root.doneFakeFlick = false + } } diff --git a/components/containers/StyledListView.qml b/components/containers/StyledListView.qml index 8b5a4401..484421e5 100644 --- a/components/containers/StyledListView.qml +++ b/components/containers/StyledListView.qml @@ -4,11 +4,28 @@ import qs.components ListView { id: root + property bool doneFakeFlick + maximumFlickVelocity: 3000 rebound: Transition { + onRunningChanged: { + if (!running && !root.doneFakeFlick) { + root.doneFakeFlick = true; + root.flick(1, 1); + root.flick(-1, -1); + Qt.callLater(() => root.cancelFlick()); + } + } + Anim { properties: "x,y" } } + + Timer { + running: root.doneFakeFlick + interval: 10 + onTriggered: root.doneFakeFlick = false + } } diff --git a/components/containers/VerticalFadeFlickable.qml b/components/containers/VerticalFadeFlickable.qml new file mode 100644 index 00000000..020972ff --- /dev/null +++ b/components/containers/VerticalFadeFlickable.qml @@ -0,0 +1,72 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.components +import qs.components.effects + +StyledFlickable { + id: root + + property real fadeAmount: 0.2 + + property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1 + property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1 + + function fadeShouldBeActive(isStart: bool): bool { + // When content is smaller than flickable size, hide fade when rebound starts + if (contentHeight + topMargin + bottomMargin < height && rebound.running && ((isStart ? verticalOvershoot > 0 : verticalOvershoot < 0))) + return false; + + if (isStart) + return visibleArea.yPosition > 0; + return visibleArea.yPosition + visibleArea.heightRatio < 1; + } + + flickableDirection: Flickable.VerticalFlick + + layer.enabled: true + layer.effect: Mask { + maskSource: mask + + Rectangle { + id: mask + + anchors.fill: parent + visible: false + layer.enabled: true + + gradient: Gradient { + orientation: Gradient.Vertical + + GradientStop { + position: 0 + color: Qt.rgba(0, 0, 0, root.topFadeOpacity) + } + GradientStop { + position: root.fadeAmount + color: Qt.rgba(0, 0, 0, 1) + } + GradientStop { + position: 1 - root.fadeAmount + color: Qt.rgba(0, 0, 0, 1) + } + GradientStop { + position: 1 + color: Qt.rgba(0, 0, 0, root.bottomFadeOpacity) + } + } + } + } + + Behavior on topFadeOpacity { + Anim { + type: Anim.SlowEffects + } + } + + Behavior on bottomFadeOpacity { + Anim { + type: Anim.SlowEffects + } + } +} diff --git a/components/controls/ButtonBase.qml b/components/controls/ButtonBase.qml new file mode 100644 index 00000000..ab263148 --- /dev/null +++ b/components/controls/ButtonBase.qml @@ -0,0 +1,95 @@ +import QtQuick +import Caelestia.Config +import qs.components +import qs.services + +StyledRect { + id: root + + enum ButtonType { + Filled, + Tonal, + Text + } + + property bool checked + property alias disabled: stateLayer.disabled + property bool isToggle + property bool isRound + + property bool radiusMorph: true + property alias shapeMorph: stateLayer.shapeMorph + property bool fillWidth // For ButtonRow + + property font font + property int type: ButtonBase.Filled + + property real padding + property real horizontalPadding: padding + property real verticalPadding: padding + + readonly property alias pressed: stateLayer.pressed + readonly property alias hovered: stateLayer.containsMouse + readonly property alias stateLayer: stateLayer + readonly property alias radiusAnim: radiusAnim + + property color activeColour + property color inactiveColour + property color activeOnColour + property color inactiveOnColour + property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) + property color disabledOnColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) + + property bool internalChecked + property real shapeMorphExpansion: shapeMorph && pressed ? 24 : 0 // Apparently it's always 24px no matter the width of the button + readonly property color onColour: disabled ? disabledOnColour : internalChecked ? activeOnColour : inactiveOnColour + + property real pressedRadius: Tokens.rounding.small + property real checkedRadius: Tokens.rounding.medium + property real defaultRadius: Tokens.rounding.large + + signal clicked + + onCheckedChanged: internalChecked = checked + + radius: { + if (radiusMorph && pressed) + return pressedRadius; + if (internalChecked) + return checkedRadius; + if (isRound) + return (height || implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale); + return defaultRadius; + } + color: type === ButtonBase.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour + + // Make size required so we don't forget to set it + required implicitWidth + required implicitHeight + + StateLayer { + id: stateLayer + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + disabled: root.disabled + onClicked: { + if (root.isToggle) + root.internalChecked = !root.internalChecked; + root.clicked(); + } + } + + Behavior on radius { + Anim { + id: radiusAnim + + type: Anim.DefaultEffects + } + } + + Behavior on shapeMorphExpansion { + Anim { + type: Anim.FastSpatial + } + } +} diff --git a/components/controls/CircularIndicator.qml b/components/controls/CircularIndicator.qml index aabda13a..7c4c8c4e 100644 --- a/components/controls/CircularIndicator.qml +++ b/components/controls/CircularIndicator.qml @@ -19,8 +19,8 @@ BusyIndicator { Completing } - property real implicitSize: Tokens.font.size.normal * 3 - property real strokeWidth: Tokens.padding.small * 0.8 + property real implicitSize: Tokens.font.body.medium.pointSize * 3 + property real strokeWidth: Tokens.padding.extraSmall property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer @@ -63,6 +63,7 @@ BusyIndicator { transitions: Transition { Anim { + type: Anim.DefaultEffects properties: "opacity,internalStrokeWidth" duration: manager.completeEndDuration * Tokens.anim.durations.scale } @@ -77,6 +78,7 @@ BusyIndicator { rotation: manager.rotation startAngle: manager.startFraction * 360 value: manager.endFraction - manager.startFraction + hasEndIndicator: false } CircularIndicatorManager { diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml index 2372c86c..a501e12b 100644 --- a/components/controls/CircularProgress.qml +++ b/components/controls/CircularProgress.qml @@ -1,69 +1,122 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Shapes +import Caelestia.Components import Caelestia.Config import qs.components import qs.services -Shape { +Item { id: root property real value property int startAngle: -90 - property int strokeWidth: Tokens.padding.smaller + property int sweepAngle: 360 + property int strokeWidth: Tokens.padding.small property int padding: 0 property int spacing: Tokens.spacing.small property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer + property alias hasEndIndicator: dot.active + + property bool wavy: false + property alias waveFrequency: wave.frequency + property alias waveAmplitude: wave.amplitudeMultiplier + property bool wavePaused + property alias waveDuration: waveProgAnim.duration readonly property real size: Math.min(width, height) - readonly property real arcRadius: (size - padding - strokeWidth) / 2 - readonly property real vValue: value || 1 / 360 + readonly property real arcRadius: (size - padding - strokeWidth * (1 + waveAmplitude * 2)) / 2 + property real clampedVal: Math.max(1 / 360, Math.min(1, isNaN(value) ? 0 : value)) // Not readonly for animations readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI) + readonly property real dotAngleRad: (startAngle + sweepAngle - gapAngle * (sweepAngle < 360 ? 0 : 1)) * Math.PI / 180 - preferredRendererType: Shape.CurveRenderer - asynchronous: true + readonly property real thickness: strokeWidth * (1 + waveAmplitude) * 2 // For consumers + property real implicitSize - ShapePath { - fillColor: "transparent" - strokeColor: root.bgColour - strokeWidth: root.strokeWidth - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + implicitWidth: implicitSize + implicitHeight: implicitSize - PathAngleArc { - startAngle: root.startAngle + 360 * root.vValue + root.gapAngle - sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2) - radiusX: root.arcRadius - radiusY: root.arcRadius - centerX: root.size / 2 - centerY: root.size / 2 - } + Shape { + preferredRendererType: Shape.CurveRenderer + asynchronous: true + opacity: Math.min(1, remainingArc.sweepAngle) - Behavior on strokeColor { - CAnim { - duration: Tokens.anim.durations.large + ShapePath { + fillColor: "transparent" + strokeColor: root.bgColour + strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth + capStyle: ShapePath.RoundCap + + PathAngleArc { + id: remainingArc + + radiusX: root.arcRadius + radiusY: root.arcRadius + centerX: root.size / 2 + centerY: root.size / 2 + startAngle: root.startAngle + root.clampedVal * root.sweepAngle + root.gapAngle + sweepAngle: Math.max(1 / 360, root.sweepAngle * (1 - root.clampedVal) - root.gapAngle * (root.sweepAngle < 360 ? 1 : 2)) + } + + Behavior on strokeColor { + CAnim {} } } } - ShapePath { - fillColor: "transparent" - strokeColor: root.fgColour - strokeWidth: root.strokeWidth - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + WavyLine { + id: wave - PathAngleArc { - startAngle: root.startAngle - sweepAngle: 360 * root.vValue - radiusX: root.arcRadius - radiusY: root.arcRadius - centerX: root.size / 2 - centerY: root.size / 2 + anchors.fill: parent + anchors.margins: -lineWidth * amplitudeMultiplier + + lineWidth: root.strokeWidth + color: root.fgColour + pathType: WavyLine.Arc + radius: root.arcRadius + startAngle: root.startAngle + fullAngle: root.sweepAngle + value: root.clampedVal + frequency: 8 + amplitudeMultiplier: root.wavy ? 0.5 : 0 + + Anim on waveProgress { + id: waveProgAnim + + running: true + paused: root.wavePaused || wave.amplitudeMultiplier === 0 + from: 0 + to: 1 + duration: 2000 + easing.type: Easing.Linear + loops: Animation.Infinite } - Behavior on strokeColor { - CAnim { - duration: Tokens.anim.durations.large + Behavior on color { + CAnim {} + } + + Behavior on amplitudeMultiplier { + Anim { + type: Anim.DefaultEffects } } } + + Loader { + id: dot + + x: root.size / 2 + root.arcRadius * Math.cos(root.dotAngleRad) - width / 2 + y: root.size / 2 + root.arcRadius * Math.sin(root.dotAngleRad) - height / 2 + + sourceComponent: StyledRect { + radius: Tokens.rounding.full + color: root.fgColour + opacity: Math.min(1, remainingArc.sweepAngle) + implicitWidth: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth) + implicitHeight: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth) + } + } } diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 80ea8fe8..3ab10c7a 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -24,7 +24,7 @@ ColumnLayout { id: sectionHeaderItem Layout.fillWidth: true - Layout.preferredHeight: Math.max(titleRow.implicitHeight + Tokens.padding.normal * 2, 48) + Layout.preferredHeight: Math.max(titleRow.implicitHeight + Tokens.padding.medium * 2, 48) RowLayout { id: titleRow @@ -32,14 +32,13 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.padding.normal - anchors.rightMargin: Tokens.padding.normal - spacing: Tokens.spacing.normal + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + spacing: Tokens.spacing.medium StyledText { text: root.title - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() } Item { @@ -50,7 +49,7 @@ ColumnLayout { text: "expand_more" rotation: root.expanded ? 180 : 0 color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.normal + fontStyle: Tokens.font.icon.medium Behavior on rotation { Anim { @@ -63,7 +62,7 @@ ColumnLayout { StateLayer { anchors.fill: parent color: Colours.palette.m3onSurface - radius: Tokens.rounding.normal + radius: Tokens.rounding.large showHoverBackground: false onClicked: { root.toggleRequested(); @@ -76,7 +75,7 @@ ColumnLayout { id: contentWrapper Layout.fillWidth: true - Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Tokens.spacing.small * 2) : 0 + Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Tokens.spacing.large) : 0 clip: true Behavior on Layout.preferredHeight { @@ -87,13 +86,15 @@ ColumnLayout { id: backgroundRect anchors.fill: parent - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer) opacity: root.showBackground && root.expanded ? 1.0 : 0.0 visible: root.showBackground Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -103,26 +104,28 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right y: Tokens.spacing.small - anchors.leftMargin: Tokens.padding.normal - anchors.rightMargin: Tokens.padding.normal + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium anchors.bottomMargin: Tokens.spacing.small spacing: Tokens.spacing.small opacity: root.expanded ? 1.0 : 0.0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } StyledText { id: descriptionText Layout.fillWidth: true - Layout.topMargin: root.description !== "" ? Tokens.spacing.smaller : 0 + Layout.topMargin: root.description !== "" ? Tokens.spacing.medium : 0 Layout.bottomMargin: root.description !== "" ? Tokens.spacing.small : 0 visible: root.description !== "" text: root.description color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small wrapMode: Text.Wrap } } diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml index d7885c30..b75272f6 100644 --- a/components/controls/CustomSpinBox.qml +++ b/components/controls/CustomSpinBox.qml @@ -73,23 +73,23 @@ RowLayout { root.isEditing = false; } - padding: Tokens.padding.small - leftPadding: Tokens.padding.normal - rightPadding: Tokens.padding.normal + padding: Tokens.padding.extraSmall + leftPadding: Tokens.padding.medium + rightPadding: Tokens.padding.medium background: StyledRect { implicitWidth: 100 - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.tPalette.m3surfaceContainerHigh } } StyledRect { - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.palette.m3primary implicitWidth: implicitHeight - implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: upIcon.implicitHeight + Tokens.padding.small StateLayer { id: upState @@ -120,11 +120,11 @@ RowLayout { } StyledRect { - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.palette.m3primary implicitWidth: implicitHeight - implicitHeight: downIcon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: downIcon.implicitHeight + Tokens.padding.small StateLayer { id: downState diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml index 1d8c85a9..5dd8b641 100644 --- a/components/controls/FilledSlider.qml +++ b/components/controls/FilledSlider.qml @@ -67,47 +67,29 @@ Slider { property bool moving - function update(): void { - animate = !moving; - binding.when = moving; - font.pointSize = moving ? Tokens.font.size.small : Tokens.font.size.larger; - font.family = moving ? Tokens.font.family.sans : Tokens.font.family.material; - } - - text: root.icon - color: Colours.palette.m3inverseOnSurface anchors.centerIn: parent + anchors.verticalCenterOffset: 1 + text: moving ? Math.round(root.value * 100) : root.icon + color: Colours.palette.m3inverseOnSurface + font: moving ? Tokens.font.body.small : Tokens.font.icon.medium - onMovingChanged: anim.restart() - - Binding { - id: binding - - target: icon - property: "text" - value: Math.round(root.value * 100) - when: false - } - - SequentialAnimation { - id: anim - - Anim { - target: icon - property: "scale" - to: 0 - duration: Tokens.anim.durations.normal / 2 - easing: Tokens.anim.standardAccel - } - ScriptAction { - script: icon.update() - } - Anim { - target: icon - property: "scale" - to: 1 - duration: Tokens.anim.durations.normal / 2 - easing: Tokens.anim.standardDecel + Behavior on moving { + SequentialAnimation { + Anim { + target: icon + property: "scale" + to: 0.3 + duration: Tokens.anim.durations.small / 2 + easing: Tokens.anim.standardAccel + } + PropertyAction {} + Anim { + target: icon + property: "scale" + to: 1 + duration: Tokens.anim.durations.normal / 2 + easing: Tokens.anim.standardDecel + } } } } diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml index 58af806a..60316083 100644 --- a/components/controls/IconButton.qml +++ b/components/controls/IconButton.qml @@ -3,80 +3,50 @@ import Caelestia.Config import qs.components import qs.services -StyledRect { +ButtonBase { id: root - enum Type { - Filled, - Tonal, - Text - } - property alias icon: label.text - property bool checked - property bool toggle - property real padding: type === IconButton.Text ? Tokens.padding.small / 2 : Tokens.padding.smaller - property alias font: label.font - property int type: IconButton.Filled - property bool disabled + readonly property alias label: label - property alias stateLayer: stateLayer - property alias label: label - property alias radiusAnim: radiusAnim + font: Tokens.font.icon.medium + padding: type === IconButton.Text ? Tokens.padding.extraSmall / 2 : Tokens.padding.small - property bool internalChecked - property color activeColour: type === IconButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary - property color inactiveColour: { - if (!toggle && type === IconButton.Filled) + activeColour: type === IconButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + inactiveColour: { + if (!isToggle && type === IconButton.Filled) return Colours.palette.m3primary; return type === IconButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer; } - property color activeOnColour: type === IconButton.Filled ? Colours.palette.m3onPrimary : type === IconButton.Tonal ? Colours.palette.m3onSecondary : Colours.palette.m3primary - property color inactiveOnColour: { - if (!toggle && type === IconButton.Filled) + activeOnColour: type === IconButton.Filled ? Colours.palette.m3onPrimary : type === IconButton.Tonal ? Colours.palette.m3onSecondary : Colours.palette.m3primary + inactiveOnColour: { + if (!isToggle && type === IconButton.Filled) return Colours.palette.m3onPrimary; return type === IconButton.Tonal ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant; } - property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) - property color disabledOnColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) - - signal clicked - - onCheckedChanged: internalChecked = checked - - radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour implicitWidth: implicitHeight - implicitHeight: label.implicitHeight + padding * 2 - - StateLayer { - id: stateLayer - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - disabled: root.disabled - onClicked: { - if (root.toggle) - root.internalChecked = !root.internalChecked; - root.clicked(); - } + implicitHeight: { + // Ensure even size so icon is centered properly + const h = label.implicitHeight + padding * 2; + if (h % 2 !== 0) + return h + 1; + return h; } MaterialIcon { id: label anchors.centerIn: parent - color: root.disabled ? root.disabledOnColour : root.internalChecked ? root.activeOnColour : root.inactiveOnColour - fill: !root.toggle || root.internalChecked ? 1 : 0 + anchors.verticalCenterOffset: 1 // AHHHHHHH material symbols whyyyy + color: root.onColour + fontStyle: root.font + fill: !root.isToggle || root.internalChecked ? 1 : 0 Behavior on fill { - Anim {} - } - } - - Behavior on radius { - Anim { - id: radiusAnim + Anim { + type: Anim.DefaultEffects + } } } } diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml index 2a1dacec..6859cb68 100644 --- a/components/controls/IconTextButton.qml +++ b/components/controls/IconTextButton.qml @@ -4,55 +4,40 @@ import Caelestia.Config import qs.components import qs.services -StyledRect { +ButtonBase { id: root - enum Type { - Filled, - Tonal, - Text - } - property alias icon: iconLabel.text property alias text: label.text - property bool checked - property bool toggle - property real horizontalPadding: Tokens.padding.normal - property real verticalPadding: Tokens.padding.smaller - property alias font: label.font - property int type: IconTextButton.Filled - property alias stateLayer: stateLayer - property alias iconLabel: iconLabel - property alias label: label + readonly property alias iconLabel: iconLabel + readonly property alias label: label - property bool internalChecked - property color activeColour: type === IconTextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary - property color inactiveColour: type === IconTextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer - property color activeOnColour: type === IconTextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary - property color inactiveOnColour: type === IconTextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small - signal clicked - - onCheckedChanged: internalChecked = checked - - radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: type === IconTextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour + activeColour: type === TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + inactiveColour: { + if (!isToggle && type === TextButton.Filled) + return Colours.palette.m3primary; + return type === TextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer; + } + activeOnColour: { + if (type === TextButton.Text) + return Colours.palette.m3primary; + return type === TextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary; + } + inactiveOnColour: { + if (!isToggle && type === TextButton.Filled) + return Colours.palette.m3onPrimary; + if (type === TextButton.Text) + return Colours.palette.m3primary; + return type === TextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer; + } implicitWidth: row.implicitWidth + horizontalPadding * 2 implicitHeight: row.implicitHeight + verticalPadding * 2 - StateLayer { - id: stateLayer - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - onClicked: { - if (root.toggle) - root.internalChecked = !root.internalChecked; - root.clicked(); - } - } - RowLayout { id: row @@ -64,11 +49,18 @@ StyledRect { Layout.alignment: Qt.AlignVCenter Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575) - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + color: root.onColour fill: root.internalChecked ? 1 : 0 + fontStyle: { + const f = Qt.font(root.font); + f.pointSize = Math.round(root.font.pointSize * 1.1); + return f; + } Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -77,11 +69,8 @@ StyledRect { Layout.alignment: Qt.AlignVCenter Layout.topMargin: -Math.round(iconLabel.fontInfo.pointSize * 0.0575) - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + color: root.onColour + font: root.font } } - - Behavior on radius { - Anim {} - } } diff --git a/components/controls/LoadingIndicator.qml b/components/controls/LoadingIndicator.qml new file mode 100644 index 00000000..8503c889 --- /dev/null +++ b/components/controls/LoadingIndicator.qml @@ -0,0 +1,116 @@ +import QtQuick +import Quickshell +import M3Shapes +import qs.components +import qs.services + +MaterialShape { + id: root + + property list shapes: { + if (containsIcon) + return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval]; + return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pentagon, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval]; + } + property int shapeIndex + property real cRotation + property real lRotation + property real thisLRotation + property bool containsIcon + + property bool animated: true + property int morphAnimRotation: 60 + property real morphScale: 0.14 + property alias rotateAnimDuration: rotateAnim.duration + + property real stiffness: 180 + property real dampingRatio: 0.6 + property real visibilityThreshold: 0.075 + + readonly property real springDuration: { + const wn = Math.sqrt(stiffness); + const r = -dampingRatio * wn; + const c = 1 / Math.sqrt(1 - dampingRatio * dampingRatio); + return Math.log(visibilityThreshold / c) / r; + } + readonly property real springMaxVelocity: { + const wn = Math.sqrt(stiffness); + const factor = Math.exp(-z * Math.acos(z) / Math.sqrt(1 - z * z)); + return wn * factor; + } + property bool springSettled: true + + function spring(t: real): var { + const wn = Math.sqrt(stiffness); + const za = dampingRatio * wn; + + const wd = wn * Math.sqrt(1 - dampingRatio * dampingRatio); + const r = za / wd; + const pos = 1 - Math.exp(-za * t) * (Math.cos(wd * t) + r * Math.sin(wd * t)); + const vel = Math.exp(-za * t) * (wn * wn / wd) * Math.sin(wd * t); + + return [pos, vel]; + } + + implicitSize: 38 + color: Colours.palette.m3primary + toShape: shapes[0] + + ElapsedTimer { + id: timer + } + + FrameAnimation { + running: root.animated && !root.springSettled + onTriggered: { + const t = timer.elapsed(); + + if (t >= root.springDuration) { + root.springSettled = true; + } else { + const [pos, vel] = root.spring(t); + root.morphProgress = Math.min(1, pos); // Overshooting the morph looks weird + root.thisLRotation = pos * root.morphAnimRotation; + root.scale = 1 + vel * root.morphScale / root.springMaxVelocity; + } + } + } + + Timer { + interval: 650 + repeat: true + triggeredOnStart: true + running: root.animated + onTriggered: { + root.beginBatchUpdate(); + root.fromShape = root.toShape; + root.shapeIndex = (root.shapeIndex + 1) % root.shapes.length; + root.toShape = root.shapes[root.shapeIndex]; + root.morphProgress = 0; + + root.rotation = root.rotation; + root.lRotation = (root.lRotation + root.thisLRotation) % 360; + root.thisLRotation = 0; + root.rotation = Qt.binding(() => root.cRotation + root.lRotation + root.thisLRotation); + + root.springSettled = false; + timer.restart(); + root.endBatchUpdate(); + } + } + + RotationAnimation on cRotation { + id: rotateAnim + + running: root.animated + from: 0 + to: 360 + easing.type: Easing.Linear + loops: Animation.Infinite + duration: 4666 + } + + Behavior on color { + CAnim {} + } +} diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index 482a4d40..db36d128 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -48,7 +48,7 @@ MouseArea { Behavior on opacity { Anim { - duration: Tokens.anim.durations.small + type: Anim.DefaultEffects } } @@ -79,7 +79,7 @@ MouseArea { return item.mapToItem(root.parent, 0, off).y + root.marginY; } - radius: Tokens.rounding.normal + radius: Tokens.rounding.large level: 2 implicitWidth: Math.max(200, column.implicitWidth + column.anchors.margins * 2) @@ -90,9 +90,7 @@ MouseArea { origin.y: root.thisSideY === Menu.Bottom ? menu.height : 0 Behavior on yScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } @@ -105,7 +103,7 @@ MouseArea { id: column anchors.fill: parent - anchors.margins: Tokens.padding.small + anchors.margins: Tokens.padding.extraSmall spacing: 0 Repeater { @@ -118,17 +116,17 @@ MouseArea { required property int index required property MenuItem modelData - readonly property bool active: modelData === root.active + readonly property bool active: modelData === root?.active Layout.fillWidth: true - implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.normal * 2 + implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.medium * 2 - radius: active ? 12 : Tokens.rounding.extraSmall // This should use a token, but tokens are currently extremely scuffed - topLeftRadius: index === 0 ? Tokens.rounding.small : radius - topRightRadius: index === 0 ? Tokens.rounding.small : radius - bottomLeftRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius - bottomRightRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius + radius: active ? Tokens.rounding.medium : Tokens.rounding.extraSmall + topLeftRadius: index === 0 ? Tokens.rounding.medium : radius + topRightRadius: index === 0 ? Tokens.rounding.medium : radius + bottomLeftRadius: index === repeater?.count - 1 ? Tokens.rounding.medium : radius + bottomRightRadius: index === repeater?.count - 1 ? Tokens.rounding.medium : radius color: Qt.alpha(Colours.palette.m3tertiaryContainer, active ? 1 : 0) @@ -156,26 +154,26 @@ MouseArea { id: menuOptionRow anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium spacing: Tokens.spacing.small MaterialIcon { Layout.alignment: Qt.AlignVCenter - text: item.modelData.icon + text: item.modelData?.icon ?? "" color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant } StyledText { Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true - text: item.modelData.text + text: item.modelData?.text ?? "" color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface } Loader { asynchronous: true Layout.alignment: Qt.AlignVCenter - active: item.modelData.trailingIcon.length > 0 + active: item.modelData?.trailingIcon.length > 0 visible: active sourceComponent: MaterialIcon { diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml index dc52c19c..edce6639 100644 --- a/components/controls/SpinBoxRow.qml +++ b/components/controls/SpinBoxRow.qml @@ -15,8 +15,8 @@ StyledRect { property var onValueModified: function (value) {} Layout.fillWidth: true - implicitHeight: row.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -30,7 +30,7 @@ StyledRect { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index 4acd98cf..2feaf615 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -12,43 +12,46 @@ Row { Tonal } - property real horizontalPadding: Tokens.padding.normal - property real verticalPadding: Tokens.padding.smaller + property real horizontalPadding: Tokens.padding.medium + property real verticalPadding: Tokens.padding.small property int type: SplitButton.Filled property bool disabled property bool menuOnTop property string fallbackIcon property string fallbackText + property real minLeftWidth property alias menuItems: menu.items property alias active: menu.active property alias expanded: menu.expanded - property alias menu: menu - property alias iconLabel: iconLabel - property alias label: label - property alias stateLayer: stateLayer + readonly property alias menu: menu + readonly property alias iconLabel: iconLabel + readonly property alias label: label + readonly property alias stateLayer: stateLayer + readonly property alias textRow: textRow + readonly property alias expandBtn: expandBtn property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer property color textColour: type == SplitButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) property color disabledTextColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) - spacing: Math.floor(Tokens.spacing.small / 2) + spacing: Math.floor(Tokens.spacing.extraSmall) StyledRect { radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - topRightRadius: Tokens.rounding.small / 2 - bottomRightRadius: Tokens.rounding.small / 2 + topRightRadius: Tokens.rounding.medium / 2 + bottomRightRadius: Tokens.rounding.medium / 2 color: root.disabled ? root.disabledColour : root.colour - implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2 + implicitWidth: Math.max(root.minLeftWidth, textRow.implicitWidth + root.horizontalPadding * 2) implicitHeight: expandBtn.implicitHeight StateLayer { id: stateLayer - rect.topRightRadius: parent.topRightRadius - rect.bottomRightRadius: parent.bottomRightRadius + topRightRadius: parent.topRightRadius + bottomRightRadius: parent.bottomRightRadius color: root.textColour disabled: root.disabled onClicked: root.active?.clicked() @@ -93,7 +96,7 @@ Row { StyledRect { id: expandBtn - property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) : Tokens.rounding.small / 2 + property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) : Tokens.rounding.medium / 2 radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) topLeftRadius: rad diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml index bd1ecc62..a907ed2b 100644 --- a/components/controls/SplitButtonRow.qml +++ b/components/controls/SplitButtonRow.qml @@ -20,8 +20,8 @@ StyledRect { signal selected(item: MenuItem) Layout.fillWidth: true - implicitHeight: row.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) clip: false @@ -33,7 +33,7 @@ StyledRect { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml index ff7772ca..5bed09a4 100644 --- a/components/controls/StyledInputField.qml +++ b/components/controls/StyledInputField.qml @@ -21,14 +21,14 @@ Item { signal editingFinished implicitWidth: 70 - implicitHeight: inputField.implicitHeight + Tokens.padding.small * 2 + implicitHeight: inputField.implicitHeight + Tokens.padding.small StyledRect { id: container anchors.fill: parent color: inputHover.containsMouse || inputField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 1 border.color: inputField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) opacity: root.enabled ? 1 : 0.5 @@ -54,7 +54,7 @@ Item { id: inputField anchors.centerIn: parent - width: parent.width - Tokens.padding.normal + width: parent.width - Tokens.padding.medium horizontalAlignment: root.horizontalAlignment validator: root.validator readOnly: root.readOnly diff --git a/components/controls/StyledProgressBar.qml b/components/controls/StyledProgressBar.qml new file mode 100644 index 00000000..399b66fd --- /dev/null +++ b/components/controls/StyledProgressBar.qml @@ -0,0 +1,247 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Templates +import Caelestia +import Caelestia.Components +import Caelestia.Config +import Caelestia.Internal +import qs.components +import qs.services + +ProgressBar { + id: root + + enum IndeterminateAnimState { + Running, + Completing, + Stopped + } + + property color fgColour: Colours.palette.m3primary + property color bgColour: Colours.palette.m3secondaryContainer + + property bool wavy + property bool wavePaused + property int waveFrequency: 6 + property real waveAmplitude: 0.5 + property int waveDuration: 1000 + + property int indeterminateAnimState: StyledProgressBar.Stopped + + function toBounds(startFrac: real, endFrac: real, gapSize: real): point { + startFrac = CUtils.clamp(startFrac, 0, 1); + endFrac = CUtils.clamp(endFrac, 0, 1); + + // Ramp down gap size + const GAP_RAMP_DOWN_THRESHOLD = 0.01; + gapSize += height / 2; + const startGapSize = (gapSize * CUtils.clamp(startFrac, 0, GAP_RAMP_DOWN_THRESHOLD) / GAP_RAMP_DOWN_THRESHOLD); + const endGapSize = (gapSize * (1 - CUtils.clamp(endFrac, 1 - GAP_RAMP_DOWN_THRESHOLD, 1)) / GAP_RAMP_DOWN_THRESHOLD); + const start = width * startFrac + startGapSize; + const end = width * endFrac - endGapSize; + + return start >= end ? Qt.point(0, 0) : Qt.point(start, end); + } + + function updateIAnimState(): void { + if (indeterminate) { + manager.completeEndProgress = 0; + indeterminateAnimState = StyledProgressBar.Running; + } else if (indeterminateAnimState === StyledProgressBar.Running) { + indeterminateAnimState = StyledProgressBar.Completing; + } + } + + onIndeterminateChanged: updateIAnimState() + Component.onCompleted: updateIAnimState() + + implicitWidth: 200 + implicitHeight: 4 + + contentItem: Loader { + anchors.fill: parent + asynchronous: true + sourceComponent: root.indeterminate || root.indeterminateAnimState !== StyledProgressBar.Stopped ? indeterminateComp : determinateComp + } + + LinearIndicatorManager { + id: manager + + gap: root.Tokens.spacing.extraSmall + + Anim on progress { + running: root.indeterminateAnimState !== StyledProgressBar.Stopped + from: 0 + to: 1 + duration: manager.duration + easing.type: Easing.Linear + loops: Animation.Infinite + } + + Anim on completeEndProgress { + running: root.indeterminateAnimState === StyledProgressBar.Completing + to: 1 + duration: manager.completeEndDuration + onFinished: { + if (root.indeterminateAnimState === StyledProgressBar.Completing) + root.indeterminateAnimState = StyledProgressBar.Stopped; + } + } + } + + Component { + id: determinateComp + + Item { + Line { + id: remaining + + anchors.right: parent.right + implicitWidth: parent.width - wave.implicitWidth - Tokens.spacing.extraSmall + } + + Line { + property real implicitSize + + Component.onCompleted: implicitSize = Qt.binding(() => parent.width - wave.implicitWidth < parent.height ? parent.height : 4) + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: (parent.height - implicitHeight) / 2 + + implicitWidth: implicitSize + implicitHeight: implicitSize + + radius: Tokens.rounding.full + color: root.fgColour + + Behavior on implicitSize { + Anim { + type: Anim.FastSpatial + } + } + } + + Wave { + id: wave + + anchors.left: parent.left + Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition) + + Behavior on implicitWidth { + Anim {} + } + } + } + } + + Component { + id: indeterminateComp + + Item { + id: content + + Line { + bounds: { + const i = manager.activeIndicators[0]; // qmllint disable unresolved-type + return i ? root.toBounds(0, i.startFraction, i.gapSize / 2) : Qt.point(0, 0); + } + } + + Line { + bounds: { + const i = manager.activeIndicators[manager.activeIndicators.length - 1]; // qmllint disable unresolved-type + return i ? root.toBounds(i.endFraction, 1, i.gapSize / 2) : Qt.point(0, 0); + } + } + + Instantiator { + model: Math.max(manager.activeIndicators.length, 1) - 1 // qmllint disable unresolved-type + + delegate: Line { + required property int index + readonly property LinearIndicatorSegment cur: manager.activeIndicators[index] // qmllint disable unresolved-type + readonly property LinearIndicatorSegment next: manager.activeIndicators[index + 1 % manager.activeIndicators.length] // qmllint disable unresolved-type + + bounds: root.toBounds(cur.endFraction, next.startFraction, cur.gapSize / 2) + } + + onObjectAdded: (_, obj) => content.data.push(obj) + onObjectRemoved: (_, obj) => { + const idx = content.data.indexOf(obj); + if (idx !== -1) + content.data.splice(idx, 1); + } + } + + Instantiator { + model: manager.activeIndicators // qmllint disable unresolved-type + + delegate: Wave { + required property LinearIndicatorSegment modelData + readonly property point bounds: root.toBounds(modelData.startFraction, modelData.endFraction, modelData.gapSize / 2) + + x: bounds.x + implicitWidth: bounds.y - bounds.x + + color: root.fgColour + } + + onObjectAdded: (_, obj) => content.data.push(obj) + onObjectRemoved: (_, obj) => { + const idx = content.data.indexOf(obj); + if (idx !== -1) + content.data.splice(idx, 1); + } + } + } + } + + component Line: StyledRect { + property point bounds + + x: bounds.x + implicitWidth: bounds.y - bounds.x + + anchors.verticalCenter: parent.verticalCenter + implicitHeight: parent.height + + radius: Tokens.rounding.full + color: root.bgColour + } + + component Wave: WavyLine { + id: wave + + anchors.verticalCenter: parent.verticalCenter + implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth + + lineWidth: parent.height + amplitudeMultiplier: root.wavy ? root.waveAmplitude : 0 + frequency: root.waveFrequency + startX: x + fullLength: parent.width + color: root.fgColour + + Anim on waveProgress { + running: true + paused: wave.amplitudeMultiplier === 0 || root.wavePaused + from: 0 + to: 1 + duration: root.waveDuration + easing.type: Easing.Linear + loops: Animation.Infinite + } + + Behavior on amplitudeMultiplier { + Anim { + type: Anim.DefaultEffects + } + } + + Behavior on color { + CAnim {} + } + } +} diff --git a/components/controls/StyledRadioButton.qml b/components/controls/StyledRadioButton.qml index 24c7d6e8..a12711b1 100644 --- a/components/controls/StyledRadioButton.qml +++ b/components/controls/StyledRadioButton.qml @@ -7,7 +7,7 @@ import qs.services RadioButton { id: root - font.pointSize: Tokens.font.size.smaller + font: Tokens.font.body.small implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) @@ -24,7 +24,7 @@ RadioButton { anchors.verticalCenter: parent.verticalCenter StateLayer { - anchors.margins: -Tokens.padding.smaller + anchors.margins: -Tokens.padding.small color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary z: -1 onClicked: root.click() @@ -46,9 +46,9 @@ RadioButton { contentItem: StyledText { text: root.text - font.pointSize: root.font.pointSize + font: root.font anchors.verticalCenter: parent.verticalCenter anchors.left: outerCircle.right - anchors.leftMargin: Tokens.spacing.smaller + anchors.leftMargin: Tokens.spacing.medium } } diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml index 8cfe3f8d..6a61391c 100644 --- a/components/controls/StyledScrollBar.qml +++ b/components/controls/StyledScrollBar.qml @@ -45,7 +45,7 @@ ScrollBar { } } } - implicitWidth: Tokens.padding.small + implicitWidth: Tokens.padding.extraSmall contentItem: StyledRect { anchors.left: parent.left @@ -74,7 +74,9 @@ ScrollBar { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/components/controls/StyledSlider.qml b/components/controls/StyledSlider.qml index f169691b..751b7586 100644 --- a/components/controls/StyledSlider.qml +++ b/components/controls/StyledSlider.qml @@ -1,5 +1,9 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Templates +import Caelestia +import Caelestia.Components import Caelestia.Config import qs.components import qs.services @@ -7,51 +11,178 @@ import qs.services Slider { id: root - background: Item { + property bool wavy + property bool animateWave + property real waveFrequency: 6 + property int waveDuration: 1000 + property int radius: Tokens.rounding.medium + + property color fgColour: enabled ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3onSurface, 0.38) + property color bgColour: enabled ? Colours.palette.m3secondaryContainer : Qt.alpha(Colours.palette.m3onSurface, 0.1) + + property real pos: visualPosition + property real filledWidth + + signal interaction(v: real) + + Component.onCompleted: filledWidth = Qt.binding(() => (width - handle.implicitWidth - handle.anchors.leftMargin) * pos) + + implicitWidth: 200 + implicitHeight: 12 + + contentItem: Item { + anchors.fill: parent + StyledRect { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.topMargin: root.implicitHeight / 3 - anchors.bottomMargin: root.implicitHeight / 3 + id: remaining - implicitWidth: root.handle.x - root.implicitHeight / 6 + anchors.left: handle.right + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Tokens.spacing.extraSmall - color: Colours.palette.m3primary - radius: Tokens.rounding.full - topRightRadius: root.implicitHeight / 15 - bottomRightRadius: root.implicitHeight / 15 + implicitHeight: parent.height * (parent.height <= 12 ? opacity : Math.min(opacity * 2, 1)) + opacity: Math.min(width, 12) / 12 + + radius: root.radius + topLeftRadius: Tokens.rounding.extraSmall / 2 + bottomLeftRadius: Tokens.rounding.extraSmall / 2 + color: root.bgColour } StyledRect { - anchors.top: parent.top - anchors.bottom: parent.bottom anchors.right: parent.right - anchors.topMargin: root.implicitHeight / 3 - anchors.bottomMargin: root.implicitHeight / 3 + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: 4 * remaining.opacity - implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 + implicitWidth: implicitHeight + implicitHeight: 4 * remaining.opacity + opacity: remaining.opacity - color: Colours.palette.m3surfaceContainerHighest radius: Tokens.rounding.full - topLeftRadius: root.implicitHeight / 15 - bottomLeftRadius: root.implicitHeight / 15 + color: root.fgColour + } + + StyledRect { + id: handle + + anchors.left: filled.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Tokens.spacing.extraSmall + + implicitWidth: 4 + implicitHeight: { + const t = CUtils.clamp((parent.height - 12) / 16, 0, 1); + const lerp = (a, b) => a + (b - a) * t; + return parent.height * (mouse.pressed ? lerp(3.5, 1.5) : lerp(3, 1.2)); + } + + radius: Tokens.rounding.full + color: root.fgColour + + Behavior on implicitHeight { + Anim { + type: Anim.FastSpatial + } + } + } + + Loader { + id: filled + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + asynchronous: true + + sourceComponent: root.wavy ? waveComp : lineComp + } + + Component { + id: lineComp + + StyledRect { + implicitWidth: root.filledWidth + implicitHeight: root.height + + radius: root.radius + topRightRadius: Tokens.rounding.extraSmall / 2 + bottomRightRadius: Tokens.rounding.extraSmall / 2 + color: root.fgColour + } + } + + Component { + id: waveComp + + WavyLine { + lineWidth: root.height * 0.7 + frequency: root.waveFrequency + startX: x + fullLength: root.width - handle.implicitWidth - handle.anchors.leftMargin + color: root.fgColour + + implicitWidth: root.filledWidth + implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth + + Anim on waveProgress { + running: true + paused: !root.animateWave + from: 0 + to: 1 + duration: root.waveDuration + easing.type: Easing.Linear + loops: Animation.Infinite + } + + Behavior on color { + CAnim {} + } + } } } - handle: StyledRect { - x: root.visualPosition * root.availableWidth - implicitWidth / 2 + Binding { + id: posBinding - implicitWidth: root.implicitHeight / 4.5 - implicitHeight: root.implicitHeight + target: root + property: "pos" + value: CUtils.clamp(mouse.pressStartPos + mouse.dragMovement, 0, 1) + when: mouse.pressed + } - color: Colours.palette.m3primary - radius: Tokens.rounding.full + MouseArea { + id: mouse - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: Qt.PointingHandCursor + property real pressStartX + property real pressStartPos + property real dragMovement + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + preventStealing: true + implicitHeight: handle.implicitHeight + + onPressed: e => { + widthBehavior.enabled = false; + pressStartX = e.x; + pressStartPos = root.visualPosition; } + onPositionChanged: e => { + dragMovement = (e.x - pressStartX) / width; + root.interaction(posBinding.value); + } + onReleased: e => { + root.interaction(posBinding.value); + widthBehavior.enabled = true; + dragMovement = 0; + } + } + + Behavior on filledWidth { + id: widthBehavior + + Anim {} } } diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml index 2e31adbf..4af5e51e 100644 --- a/components/controls/StyledSwitch.qml +++ b/components/controls/StyledSwitch.qml @@ -18,17 +18,17 @@ Switch { color: root.checked ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHighest, root.cLayer) implicitWidth: implicitHeight * 1.7 - implicitHeight: Tokens.font.size.normal + Tokens.padding.smaller * 2 + implicitHeight: Tokens.font.body.medium.pointSize + Tokens.padding.small * 2 StyledRect { - readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight + readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.2 : implicitHeight radius: Tokens.rounding.full color: root.checked ? Colours.palette.m3onPrimary : Colours.layer(Colours.palette.m3outline, root.cLayer + 1) - x: root.checked ? parent.implicitWidth - nonAnimWidth - Tokens.padding.small / 2 : Tokens.padding.small / 2 + x: root.checked ? parent.implicitWidth - nonAnimWidth - Tokens.padding.extraSmall / 2 : Tokens.padding.extraSmall / 2 implicitWidth: nonAnimWidth - implicitHeight: parent.implicitHeight - Tokens.padding.small + implicitHeight: parent.implicitHeight - Tokens.padding.extraSmall anchors.verticalCenter: parent.verticalCenter StyledRect { @@ -39,7 +39,9 @@ Switch { opacity: root.pressed ? 0.1 : root.hovered ? 0.08 : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -83,12 +85,12 @@ Switch { anchors.centerIn: parent width: height - height: parent.implicitHeight - Tokens.padding.small * 2 + height: parent.implicitHeight - Tokens.padding.medium preferredRendererType: Shape.CurveRenderer asynchronous: true ShapePath { - strokeWidth: root.Tokens.font.size.larger * 0.15 + strokeWidth: root.Tokens.font.body.large.pointSize * 0.15 strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest fillColor: "transparent" capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap @@ -129,11 +131,15 @@ Switch { } Behavior on x { - Anim {} + Anim { + type: Anim.FastSpatial + } } Behavior on implicitWidth { - Anim {} + Anim { + type: Anim.FastSpatial + } } } } @@ -145,7 +151,7 @@ Switch { } component PropAnim: PropertyAnimation { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standard + duration: Tokens.anim.durations.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml index 4ab2f824..85cf4fa6 100644 --- a/components/controls/StyledTextField.qml +++ b/components/controls/StyledTextField.qml @@ -11,8 +11,7 @@ TextField { color: Colours.palette.m3onSurface placeholderTextColor: Colours.palette.m3outline - font.family: Tokens.font.family.sans - font.pointSize: Tokens.font.size.smaller + font: Tokens.font.body.small renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering cursorVisible: !readOnly @@ -25,7 +24,7 @@ TextField { implicitWidth: 2 color: Colours.palette.m3primary - radius: Tokens.rounding.normal + radius: Tokens.rounding.large Connections { function onCursorPositionChanged(): void { diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index b909739f..d4d83183 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -13,8 +13,8 @@ StyledRect { signal toggled(checked: bool) Layout.fillWidth: true - implicitHeight: row.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -28,7 +28,7 @@ StyledRect { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml index 94d76ff8..5cfff6e8 100644 --- a/components/controls/TextButton.qml +++ b/components/controls/TextButton.qml @@ -3,75 +3,41 @@ import Caelestia.Config import qs.components import qs.services -StyledRect { +ButtonBase { id: root - enum Type { - Filled, - Tonal, - Text - } - property alias text: label.text - property bool checked - property bool toggle - property real horizontalPadding: Tokens.padding.normal - property real verticalPadding: Tokens.padding.smaller - property alias font: label.font - property int type: TextButton.Filled + readonly property alias label: label - property alias stateLayer: stateLayer - property alias label: label + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small - property bool internalChecked - property color activeColour: type === TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary - property color inactiveColour: { - if (!toggle && type === TextButton.Filled) + activeColour: type === TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + inactiveColour: { + if (!isToggle && type === TextButton.Filled) return Colours.palette.m3primary; return type === TextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer; } - property color activeOnColour: { + activeOnColour: { if (type === TextButton.Text) return Colours.palette.m3primary; return type === TextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary; } - property color inactiveOnColour: { - if (!toggle && type === TextButton.Filled) + inactiveOnColour: { + if (!isToggle && type === TextButton.Filled) return Colours.palette.m3onPrimary; if (type === TextButton.Text) return Colours.palette.m3primary; return type === TextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer; } - signal clicked - - onCheckedChanged: internalChecked = checked - - radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: type === TextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour - implicitWidth: label.implicitWidth + horizontalPadding * 2 implicitHeight: label.implicitHeight + verticalPadding * 2 - StateLayer { - id: stateLayer - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - onClicked: { - if (root.toggle) - root.internalChecked = !root.internalChecked; - root.clicked(); - } - } - StyledText { id: label anchors.centerIn: parent - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - } - - Behavior on radius { - Anim {} + color: root.onColour } } diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index 232426c1..cd6427a2 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -14,9 +14,9 @@ StyledRect { property string icon property string label property string accent: "Secondary" - property real iconSize: Tokens.font.size.large + property real iconSize: Tokens.font.icon.large.pointSize property real horizontalPadding: Tokens.padding.large - property real verticalPadding: Tokens.padding.normal + property real verticalPadding: Tokens.padding.medium property string tooltip: "" property bool hovered: false @@ -24,10 +24,10 @@ StyledRect { Component.onCompleted: hovered = toggleStateLayer.containsMouse - Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Tokens.padding.normal * 2 : toggled ? Tokens.padding.small * 2 : 0) + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Tokens.padding.medium * 2 : toggled ? Tokens.padding.small : 0) implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 - radius: toggled || toggleStateLayer.pressed ? Tokens.rounding.small : Math.min(width, height) / 2 * Math.min(1, Tokens.rounding.scale) + radius: toggled || toggleStateLayer.pressed ? Tokens.rounding.medium : Math.min(width, height) / 2 * Math.min(1, Tokens.rounding.scale) color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] Connections { @@ -52,7 +52,7 @@ StyledRect { id: toggleBtnInner anchors.centerIn: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { id: toggleBtnIcon @@ -61,10 +61,12 @@ StyledRect { fill: root.toggled ? 1 : 0 text: root.icon color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] - font.pointSize: root.iconSize + fontStyle: Tokens.font.icon.size(root.iconSize).build() Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/components/controls/ToggleRow.qml b/components/controls/ToggleRow.qml index beb6a15c..133c3ff4 100644 --- a/components/controls/ToggleRow.qml +++ b/components/controls/ToggleRow.qml @@ -12,7 +12,7 @@ RowLayout { property alias toggle: toggle Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index 6c62f43d..162bd691 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -47,7 +47,7 @@ Popup { let newY = targetPos.y - tooltipHeight - Tokens.spacing.small; // Keep within bounds - const padding = Tokens.padding.normal; + const padding = Tokens.padding.medium; if (newX < padding) { newX = padding; } else if (newX + tooltipWidth > (parent.width - padding)) { @@ -116,11 +116,11 @@ Popup { contentItem: StyledRect { id: tooltipRect - implicitWidth: tooltipText.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: tooltipText.implicitHeight + Tokens.padding.smaller * 2 + implicitWidth: tooltipText.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: tooltipText.implicitHeight + Tokens.padding.large color: Colours.palette.m3surfaceContainerHighest - radius: Tokens.rounding.small + radius: Tokens.rounding.medium antialiasing: true // Add elevation for depth @@ -138,7 +138,7 @@ Popup { text: root.text color: Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } } diff --git a/components/effects/Elevation.qml b/components/effects/Elevation.qml index 9e0962da..9b8822c5 100644 --- a/components/effects/Elevation.qml +++ b/components/effects/Elevation.qml @@ -13,6 +13,8 @@ RectangularShadow { offset.y: dp / 2 Behavior on dp { - Anim {} + Anim { + type: Anim.SlowEffects + } } } diff --git a/components/effects/InnerBorder.qml b/components/effects/InnerBorder.qml index 1b502e29..c16800f0 100644 --- a/components/effects/InnerBorder.qml +++ b/components/effects/InnerBorder.qml @@ -37,8 +37,8 @@ StyledRect { id: maskInner anchors.fill: parent - anchors.margins: Tokens.padding.normal - radius: Tokens.rounding.normal + anchors.margins: Tokens.padding.medium + radius: Tokens.rounding.large } } } diff --git a/components/effects/Mask.qml b/components/effects/Mask.qml new file mode 100644 index 00000000..fb463adf --- /dev/null +++ b/components/effects/Mask.qml @@ -0,0 +1,7 @@ +import QtQuick.Effects + +MultiEffect { + maskEnabled: true + maskSpreadAtMin: 1 + maskThresholdMin: 0.5 +} diff --git a/components/effects/OpacityMask.qml b/components/effects/OpacityMask.qml deleted file mode 100644 index 8e625034..00000000 --- a/components/effects/OpacityMask.qml +++ /dev/null @@ -1,9 +0,0 @@ -import QtQuick -import Quickshell - -ShaderEffect { - required property Item source - required property Item maskSource - - fragmentShader: Quickshell.shellPath("assets/shaders/opacitymask.frag.qsb") -} diff --git a/components/filedialog/CurrentItem.qml b/components/filedialog/CurrentItem.qml index 83cb4a92..4151c8e0 100644 --- a/components/filedialog/CurrentItem.qml +++ b/components/filedialog/CurrentItem.qml @@ -9,8 +9,8 @@ Item { required property var currentItem - implicitWidth: content.implicitWidth + Tokens.padding.larger + content.anchors.rightMargin - implicitHeight: currentItem ? content.implicitHeight + Tokens.padding.normal + content.anchors.bottomMargin : 0 + implicitWidth: content.implicitWidth + Tokens.padding.medium + content.anchors.rightMargin + implicitHeight: currentItem ? content.implicitHeight + Tokens.padding.medium + content.anchors.bottomMargin : 0 Shape { preferredRendererType: Shape.CurveRenderer @@ -18,7 +18,7 @@ Item { ShapePath { id: path - readonly property real rounding: Tokens.rounding.small + readonly property real rounding: root.Tokens.rounding.medium readonly property bool flatten: root.implicitHeight < rounding * 2 readonly property real roundingY: flatten ? root.implicitHeight / 2 : rounding @@ -76,8 +76,8 @@ Item { anchors.right: parent.right anchors.bottom: parent.bottom - anchors.rightMargin: Tokens.padding.larger - Tokens.padding.small - anchors.bottomMargin: Tokens.padding.normal - Tokens.padding.small + anchors.rightMargin: Tokens.padding.medium - Tokens.padding.extraSmall + anchors.bottomMargin: Tokens.padding.medium - Tokens.padding.extraSmall Connections { function onCurrentItemChanged(): void { diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml index b46b4f72..fe686e4a 100644 --- a/components/filedialog/DialogButtons.qml +++ b/components/filedialog/DialogButtons.qml @@ -9,7 +9,7 @@ StyledRect { required property var dialog required property FolderContents folder - implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.medium * 2 color: Colours.tPalette.m3surfaceContainer @@ -17,7 +17,7 @@ StyledRect { id: inner anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium spacing: Tokens.spacing.small @@ -28,14 +28,14 @@ StyledRect { StyledRect { Layout.fillWidth: true Layout.fillHeight: true - Layout.rightMargin: Tokens.spacing.normal + Layout.rightMargin: Tokens.spacing.medium color: Colours.tPalette.m3surfaceContainerHigh - radius: Tokens.rounding.small + radius: Tokens.rounding.medium StyledText { anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium text: `${root.dialog.filterLabel} (${root.dialog.filters.map(f => `*.${f}`).join(", ")})` } @@ -43,10 +43,10 @@ StyledRect { StyledRect { color: Colours.tPalette.m3surfaceContainerHigh - radius: Tokens.rounding.small + radius: Tokens.rounding.medium - implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 + implicitWidth: cancelText.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: cancelText.implicitHeight + Tokens.padding.medium * 2 StateLayer { disabled: !root.dialog.selectionValid @@ -57,7 +57,7 @@ StyledRect { id: selectText anchors.centerIn: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium text: qsTr("Select") color: root.dialog.selectionValid ? Colours.palette.m3onSurface : Colours.palette.m3outline @@ -66,10 +66,10 @@ StyledRect { StyledRect { color: Colours.tPalette.m3surfaceContainerHigh - radius: Tokens.rounding.small + radius: Tokens.rounding.medium - implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 + implicitWidth: cancelText.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: cancelText.implicitHeight + Tokens.padding.medium * 2 StateLayer { onClicked: { @@ -81,7 +81,7 @@ StyledRect { id: cancelText anchors.centerIn: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium text: qsTr("Cancel") } diff --git a/components/filedialog/FileDialog.qml b/components/filedialog/FileDialog.qml index 90dd2bc2..45f814a4 100644 --- a/components/filedialog/FileDialog.qml +++ b/components/filedialog/FileDialog.qml @@ -50,7 +50,10 @@ LazyLoader { implicitWidth: 1000 implicitHeight: 600 + minimumSize.width: 400 + minimumSize.height: 300 color: Colours.tPalette.m3surface + surfaceFormat.opaque: false title: loader.title onVisibleChanged: { diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index d8869b83..6238535d 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Effects import QtQuick.Layouts import Quickshell import Caelestia.Config import Caelestia.Models import qs.components import qs.components.controls +import qs.components.effects import qs.components.filedialog import qs.components.images import qs.services @@ -24,12 +24,9 @@ Item { color: Colours.tPalette.m3surfaceContainer layer.enabled: true - layer.effect: MultiEffect { + layer.effect: Mask { maskSource: mask - maskEnabled: true maskInverted: true - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 } } @@ -42,8 +39,8 @@ Item { Rectangle { anchors.fill: parent - anchors.margins: Tokens.padding.small - radius: Tokens.rounding.small + anchors.margins: Tokens.padding.extraSmall + radius: Tokens.rounding.medium } } @@ -59,20 +56,20 @@ Item { Layout.alignment: Qt.AlignHCenter text: "scan_delete" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.extraLarge * 2 - font.weight: 500 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.Medium).build() } StyledText { text: qsTr("This folder is empty") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -80,10 +77,10 @@ Item { id: view anchors.fill: parent - anchors.margins: Tokens.padding.small + Tokens.padding.normal + anchors.margins: Tokens.padding.extraSmall + Tokens.padding.medium cellWidth: Sizes.itemWidth + Tokens.spacing.small - cellHeight: Sizes.itemWidth + Tokens.spacing.small * 2 + Tokens.padding.normal * 2 + 1 + cellHeight: Sizes.itemWidth + Tokens.spacing.large + Tokens.padding.medium * 2 + 1 clip: true focus: true @@ -120,12 +117,12 @@ Item { properties: "opacity,scale" from: 0 to: 1 - type: Anim.DefaultSpatial } } remove: Transition { Anim { + type: Anim.DefaultEffects property: "opacity" to: 0 } @@ -137,13 +134,13 @@ Item { displaced: Transition { Anim { + type: Anim.DefaultEffects properties: "opacity,scale" to: 1 easing: Tokens.anim.standardDecel } Anim { properties: "x,y" - type: Anim.DefaultSpatial } } } @@ -151,7 +148,7 @@ Item { CurrentItem { anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Tokens.padding.small + anchors.margins: Tokens.padding.extraSmall currentItem: view.currentItem } @@ -162,12 +159,12 @@ Item { required property int index required property FileSystemEntry modelData - readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Tokens.padding.normal * 2 + readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Tokens.padding.medium * 2 implicitWidth: Sizes.itemWidth implicitHeight: nonAnimHeight - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 clip: true @@ -187,9 +184,9 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top - anchors.topMargin: Tokens.padding.normal + anchors.topMargin: Tokens.padding.medium - implicitSize: Sizes.itemWidth - Tokens.padding.normal * 2 + implicitSize: Sizes.itemWidth - Tokens.padding.medium * 2 Component.onCompleted: { const file = item.modelData; @@ -211,7 +208,7 @@ Item { anchors.right: parent.right anchors.top: icon.bottom anchors.topMargin: Tokens.spacing.small - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium horizontalAlignment: Text.AlignHCenter elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml index c53d8f76..19cb75a5 100644 --- a/components/filedialog/HeaderBar.qml +++ b/components/filedialog/HeaderBar.qml @@ -11,8 +11,8 @@ StyledRect { required property var dialog - implicitWidth: inner.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 + implicitWidth: inner.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.medium * 2 color: Colours.tPalette.m3surfaceContainer @@ -20,15 +20,15 @@ StyledRect { id: inner anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium spacing: Tokens.spacing.small Item { implicitWidth: implicitHeight - implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: upIcon.implicitHeight + Tokens.padding.small StateLayer { - radius: Tokens.rounding.small + radius: Tokens.rounding.medium disabled: root.dialog.cwd.length === 1 onClicked: root.dialog.cwd.pop() } @@ -46,7 +46,7 @@ StyledRect { StyledRect { Layout.fillWidth: true - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.tPalette.m3surfaceContainerHigh implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2 @@ -55,7 +55,7 @@ StyledRect { id: pathComponents anchors.fill: parent - anchors.margins: Tokens.padding.small / 2 + anchors.margins: Tokens.padding.extraSmall / 2 anchors.leftMargin: 0 spacing: Tokens.spacing.small @@ -78,13 +78,13 @@ StyledRect { sourceComponent: StyledText { text: "/" color: Colours.palette.m3onSurfaceVariant - font.bold: true + font: Tokens.font.body.builders.small.weight(Font.Bold).build() } } Item { - implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Tokens.padding.small : 0) + folderName.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: folderName.implicitHeight + Tokens.padding.small * 2 + implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Tokens.padding.extraSmall : 0) + folderName.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: folderName.implicitHeight + Tokens.padding.small Loader { asynchronous: true @@ -95,7 +95,7 @@ StyledRect { root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); } - radius: Tokens.rounding.small + radius: Tokens.rounding.medium } } @@ -106,7 +106,7 @@ StyledRect { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.padding.normal + anchors.leftMargin: Tokens.padding.medium active: folder.index === 0 && folder.modelData === "Home" sourceComponent: MaterialIcon { @@ -121,11 +121,11 @@ StyledRect { anchors.left: homeIcon.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: homeIcon.active ? Tokens.padding.small : 0 + anchors.leftMargin: homeIcon.active ? Tokens.padding.extraSmall : 0 text: folder.modelData color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface - font.bold: true + font: Tokens.font.body.builders.small.weight(Font.Bold).build() } } } diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml index e9c0918b..a8d2cf15 100644 --- a/components/filedialog/Sidebar.qml +++ b/components/filedialog/Sidebar.qml @@ -13,7 +13,7 @@ StyledRect { required property var dialog implicitWidth: Sizes.sidebarWidth - implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.medium * 2 color: Colours.tPalette.m3surfaceContainer @@ -23,17 +23,16 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Tokens.padding.normal - spacing: Tokens.spacing.small / 2 + anchors.margins: Tokens.padding.medium + spacing: Tokens.spacing.extraSmall StyledText { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Tokens.padding.small / 2 - Layout.bottomMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.padding.extraSmall / 2 + Layout.bottomMargin: Tokens.spacing.medium text: qsTr("Files") color: Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.larger - font.bold: true + font: Tokens.font.body.builders.large.weight(Font.Bold).build() } Repeater { @@ -46,7 +45,7 @@ StyledRect { readonly property bool selected: modelData === root.dialog.cwd[root.dialog.cwd.length - 1] Layout.fillWidth: true - implicitHeight: placeInner.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: placeInner.implicitHeight + Tokens.padding.medium * 2 radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0) @@ -65,11 +64,11 @@ StyledRect { id: placeInner anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium anchors.leftMargin: Tokens.padding.large anchors.rightMargin: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { text: { @@ -91,11 +90,13 @@ StyledRect { return "folder"; } color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.medium fill: place.selected ? 1 : 0 Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -103,7 +104,7 @@ StyledRect { Layout.fillWidth: true text: place.modelData color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.small elide: Text.ElideRight } } diff --git a/components/filedialog/Sizes.qml b/components/filedialog/Sizes.qml index 2ad31f9f..c75202e3 100644 --- a/components/filedialog/Sizes.qml +++ b/components/filedialog/Sizes.qml @@ -4,5 +4,5 @@ import Quickshell Singleton { property int itemWidth: 103 - property int sidebarWidth: 200 + property int sidebarWidth: 230 } diff --git a/components/images/FadeImage.qml b/components/images/FadeImage.qml new file mode 100644 index 00000000..a397863b --- /dev/null +++ b/components/images/FadeImage.qml @@ -0,0 +1,75 @@ +import QtQuick +import Quickshell +import qs.components + +Image { + id: root + + property bool hadPrevious + property bool fadingOut + property bool preventInit + property int fadeOutAnim: Anim.FastEffects + property int fadeInAnim: Anim.DefaultEffects + property int fadeInLargeAnim: Anim.StandardLarge + + function maybeStartInAnim(): void { + if (!preventInit && !opacityInAnim.running && status === Image.Ready) { + opacityInAnim.type = hadPrevious ? fadeInAnim : fadeInLargeAnim; + opacityInAnim.start(); + } + } + + asynchronous: true + fillMode: Image.PreserveAspectCrop + + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } + + retainWhileLoading: true + opacity: 0 + + onStatusChanged: maybeStartInAnim() + onPreventInitChanged: maybeStartInAnim() + + Anim on opacity { + id: opacityInAnim + + running: false + to: 1 + } + + Behavior on source { + SequentialAnimation { + ScriptAction { + script: opacityInAnim.stop() + } + PropertyAction { + target: root + property: "fadingOut" + value: true + } + Anim { + target: root + property: "opacity" + to: 0 + type: root.fadeOutAnim + } + PropertyAction { + target: root + property: "fadingOut" + value: false + } + PropertyAction { + target: root + property: "hadPrevious" + value: root.source + } + PropertyAction {} + ScriptAction { + script: root.maybeStartInAnim() + } + } + } +} diff --git a/components/widgets/CoverArt.qml b/components/widgets/CoverArt.qml new file mode 100644 index 00000000..1c7fbdb0 --- /dev/null +++ b/components/widgets/CoverArt.qml @@ -0,0 +1,107 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import M3Shapes +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.images +import qs.services + +Item { + id: root + + readonly property alias shape: shape + + property bool hadPrevious + property color fallbackColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + + // Slight glow to separate from bg + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + blurMax: 1 + shadowColor: Colours.palette.m3outline + shadowOpacity: 0.3 + } + + Behavior on fallbackColour { + CAnim {} + } + + Item { + id: shapeWrapper + + anchors.fill: parent + layer.enabled: true + opacity: root.fallbackColour.a + + MaterialShape { + id: shape + + implicitSize: root.width + shape: MaterialShape.Cookie12Sided + color: Qt.alpha(root.fallbackColour, 1) + + Anim on rotation { + running: true + paused: !Players.active?.isPlaying + from: 360 + to: 0 + duration: 23500 + easing.type: Easing.Linear + loops: Animation.Infinite + } + } + } + + MaterialIcon { + anchors.centerIn: parent + + grade: 200 + text: image.status === Image.Error ? "broken_image" : "art_track" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.size((parent.width * 0.35) || 1).build() + opacity: image.status === Image.Null || image.status === Image.Error ? 1 : 0 + animate: true + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Loader { + anchors.centerIn: parent + asynchronous: true + active: opacity > 0 + opacity: image.status === Image.Loading ? 1 : 0 + + sourceComponent: LoadingIndicator { + implicitSize: root.width * 0.3 + color: Colours.palette.m3primaryContainer + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + FadeImage { + id: image + + anchors.fill: parent + + source: Players.getArtUrl(Players.active) + + layer.enabled: true + layer.effect: Mask { + maskSource: shapeWrapper + } + } +} diff --git a/components/widgets/ExtraIndicator.qml b/components/widgets/ExtraIndicator.qml index d426eed8..5bd7cb2f 100644 --- a/components/widgets/ExtraIndicator.qml +++ b/components/widgets/ExtraIndicator.qml @@ -8,13 +8,13 @@ StyledRect { required property int extra anchors.right: parent.right - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium color: Colours.palette.m3tertiary - radius: Tokens.rounding.small + radius: Tokens.rounding.medium - implicitWidth: count.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: count.implicitHeight + Tokens.padding.small * 2 + implicitWidth: count.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: count.implicitHeight + Tokens.padding.small opacity: extra > 0 ? 1 : 0 scale: extra > 0 ? 1 : 0.5 @@ -38,6 +38,7 @@ StyledRect { Behavior on opacity { Anim { + type: Anim.DefaultEffects duration: Tokens.anim.durations.expressiveFastSpatial } } diff --git a/components/widgets/WavyTopRect.qml b/components/widgets/WavyTopRect.qml new file mode 100644 index 00000000..4d42e7b7 --- /dev/null +++ b/components/widgets/WavyTopRect.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Shapes +import qs.components +import qs.services + +Shape { + id: root + + property color color: Colours.palette.m3surfaceContainer + property int waves: 4 + property real amplitude: 3 + + readonly property real waveHeight: amplitude * 2 + + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + ShapePath { + strokeWidth: 0 + strokeColor: "transparent" + fillColor: root.color + + PathSvg { + path: { + const w = root.width; + const h = root.height; + const a = root.amplitude; + const n = Math.max(1, root.waves); + const wl = w / n; + const half = wl / 2; + + let d = `M 0,${a} `; + for (let i = 0; i < n; ++i) { + const x = i * wl; + d += `Q ${x + half / 2},${-a} ${x + half},${a} `; + d += `Q ${x + half + half / 2},${3 * a} ${x + wl},${a} `; + } + d += `L ${w},${h} L 0,${h} Z`; + return d; + } + } + + Behavior on fillColor { + CAnim {} + } + } +} diff --git a/modules/GSFLoader.qml b/modules/GSFLoader.qml new file mode 100644 index 00000000..179d59fc --- /dev/null +++ b/modules/GSFLoader.qml @@ -0,0 +1,6 @@ +import QtQuick +import Quickshell + +FontLoader { + source: Quickshell.shellPath("assets/google-sans-flex/GoogleSansFlex-VariableFont_GRAD,ROND,opsz,slnt,wdth,wght.ttf") +} diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index 9ee79676..f49fa0e0 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -4,7 +4,7 @@ import Quickshell.Io import Caelestia import qs.components.misc import qs.services -import qs.modules.controlcenter +import qs.modules.nexus Scope { id: root @@ -15,8 +15,8 @@ Scope { // qmllint disable unresolved-type CustomShortcut { // qmllint enable unresolved-type - name: "controlCenter" - description: "Open control center" + name: "nexus" + description: "Open nexus" onPressed: WindowFactory.create() } @@ -133,7 +133,7 @@ Scope { WindowFactory.create(); } - target: "controlCenter" + target: "nexus" } IpcHandler { diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index 7a13150c..07777fba 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Effects import Quickshell import Quickshell.Io import Quickshell.Wayland import Caelestia import qs.components +import qs.components.effects import qs.services MouseArea { @@ -168,17 +168,17 @@ MouseArea { to: 0 type: Anim.StandardLarge } - ExAnim { + Anim { target: root properties: "rsx,rsy" to: 0 } - ExAnim { + Anim { target: root property: "sw" to: root.screen.width } - ExAnim { + Anim { target: root property: "sh" to: root.screen.height @@ -230,12 +230,9 @@ MouseArea { opacity: 0.3 layer.enabled: true - layer.effect: MultiEffect { + layer.effect: Mask { maskSource: selectionWrapper - maskEnabled: true maskInverted: true - maskSpreadAtMin: 1 - maskThresholdMin: 0.5 } } @@ -284,28 +281,24 @@ MouseArea { Behavior on rsx { enabled: !root.pressed - ExAnim {} + Anim {} } Behavior on rsy { enabled: !root.pressed - ExAnim {} + Anim {} } Behavior on sw { enabled: !root.pressed - ExAnim {} + Anim {} } Behavior on sh { enabled: !root.pressed - ExAnim {} - } - - component ExAnim: Anim { - type: Anim.DefaultSpatial + Anim {} } } diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 70f6914d..c361123f 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -57,8 +57,8 @@ Variants { asynchronous: true active: Config.background.desktopClock.enabled - anchors.margins: Tokens.padding.large * 2 - anchors.leftMargin: Tokens.padding.large * 2 + Tokens.sizes.bar.innerWidth + Math.max(Tokens.padding.smaller, Config.border.thickness) + anchors.margins: Tokens.padding.extraLargeIncreased + anchors.leftMargin: Tokens.padding.extraLargeIncreased + Tokens.sizes.bar.innerWidth + Math.max(Tokens.padding.small, Config.border.thickness) state: Config.background.desktopClock.position states: [ diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index c7415be8..6a14d41e 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -24,7 +24,7 @@ Item { readonly property color safeTertiary: useLightSet ? Colours.palette.m3tertiaryContainer : Colours.palette.m3tertiary implicitWidth: layout.implicitWidth + (Tokens.padding.large * 4 * root.clockScale) - implicitHeight: layout.implicitHeight + (Tokens.padding.large * 2 * root.clockScale) + implicitHeight: layout.implicitHeight + (Tokens.padding.extraLargeIncreased * root.clockScale) Item { id: clockContainer @@ -63,7 +63,7 @@ Item { visible: root.bgEnabled anchors.fill: parent - radius: Tokens.rounding.large * root.clockScale + radius: Tokens.rounding.extraLarge * root.clockScale opacity: Config.background.desktopClock.background.opacity color: Colours.palette.m3surface @@ -74,21 +74,20 @@ Item { id: layout anchors.centerIn: parent - spacing: Tokens.spacing.larger * root.clockScale + spacing: Tokens.spacing.large * root.clockScale RowLayout { spacing: Tokens.spacing.small StyledText { text: Time.hourStr - font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale - font.weight: Font.Bold + font: Tokens.font.clock.size(Tokens.font.headline.medium.pointSize * 3 * root.clockScale).weight(Font.Bold).build() color: root.safePrimary } StyledText { text: ":" - font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale + font: Tokens.font.clock.size(Tokens.font.headline.medium.pointSize * 3 * root.clockScale).build() color: root.safeTertiary opacity: 0.8 Layout.topMargin: -Tokens.padding.large * 1.5 * root.clockScale @@ -96,8 +95,7 @@ Item { StyledText { text: Time.minuteStr - font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale - font.weight: Font.Bold + font: Tokens.font.clock.size(Tokens.font.headline.medium.pointSize * 3 * root.clockScale).weight(Font.Bold).build() color: root.safeSecondary } @@ -111,7 +109,7 @@ Item { sourceComponent: StyledText { text: Time.amPmStr - font.pointSize: Tokens.font.size.large * root.clockScale + font: Tokens.font.clock.size(Tokens.font.title.medium.pointSize * root.clockScale).build() color: root.safeSecondary } } @@ -120,8 +118,8 @@ Item { StyledRect { Layout.fillHeight: true Layout.preferredWidth: 4 * root.clockScale - Layout.topMargin: Tokens.spacing.larger * root.clockScale - Layout.bottomMargin: Tokens.spacing.larger * root.clockScale + Layout.topMargin: Tokens.spacing.large * root.clockScale + Layout.bottomMargin: Tokens.spacing.large * root.clockScale radius: Tokens.rounding.full color: root.safePrimary opacity: 0.8 @@ -132,24 +130,19 @@ Item { StyledText { text: Time.format("MMMM").toUpperCase() - font.pointSize: Tokens.font.size.large * root.clockScale - font.letterSpacing: 4 - font.weight: Font.Bold + font: Tokens.font.clock.size(Tokens.font.title.medium.pointSize * root.clockScale).letterSpacing(4).weight(Font.Bold).build() color: root.safeSecondary } StyledText { text: Time.format("dd") - font.pointSize: Tokens.font.size.extraLarge * root.clockScale - font.letterSpacing: 2 - font.weight: Font.Medium + font: Tokens.font.clock.size(Tokens.font.headline.medium.pointSize * root.clockScale).letterSpacing(2).weight(Font.Medium).build() color: root.safePrimary } StyledText { text: Time.format("dddd") - font.pointSize: Tokens.font.size.larger * root.clockScale - font.letterSpacing: 2 + font: Tokens.font.clock.size(Tokens.font.body.large.pointSize * root.clockScale).letterSpacing(2).build() color: root.safeSecondary } } @@ -157,9 +150,7 @@ Item { } Behavior on clockScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on implicitWidth { diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index 45055879..fc163379 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -65,8 +65,8 @@ Item { values: Audio.cava.values primaryColor: Qt.alpha(Colours.palette.m3primary, 0.7) secondaryColor: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) - rounding: Tokens.rounding.small * Config.background.visualiser.rounding - spacing: Tokens.spacing.small * Config.background.visualiser.spacing + rounding: Tokens.rounding.medium * Config.background.visualiser.rounding + spacing: Tokens.spacing.extraSmall * Config.background.visualiser.spacing animationDuration: Tokens.anim.durations.normal Behavior on anchors.leftMargin { @@ -87,6 +87,8 @@ Item { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index 8cc2df9d..21ff34eb 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -12,22 +12,24 @@ Item { id: root property string source: Wallpapers.current - property Image current: one + property CachingImage current property bool completed onSourceChanged: { if (!source) current = null; - else if (current === one) - two.update(); else - one.update(); + current = imgComp.createObject(this, { + path: source + }); } Component.onCompleted: { if (source) Qt.callLater(() => { - one.update(); + current = imgComp.createObject(this, { + path: source + }); completed = true; }); } @@ -43,12 +45,12 @@ Item { Row { anchors.centerIn: parent - spacing: Tokens.spacing.large + spacing: Tokens.spacing.largeIncreased MaterialIcon { text: "sentiment_stressed" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.extraLarge * 5 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(5).build() } Column { @@ -58,13 +60,12 @@ Item { StyledText { text: qsTr("Wallpaper missing?") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.extraLarge * 2 - font.bold: true + font: Tokens.font.body.builders.large.size(28 * 2).weight(Font.Bold).build() } StyledRect { - implicitWidth: selectWallText.implicitWidth + Tokens.padding.large * 2 - implicitHeight: selectWallText.implicitHeight + Tokens.padding.small * 2 + implicitWidth: selectWallText.implicitWidth + Tokens.padding.extraLargeIncreased + implicitHeight: selectWallText.implicitHeight + Tokens.padding.small radius: Tokens.rounding.full color: Colours.palette.m3primary @@ -91,7 +92,7 @@ Item { text: qsTr("Set it now!") color: Colours.palette.m3onPrimary - font.pointSize: Tokens.font.size.large + font: Tokens.font.body.large } } } @@ -99,48 +100,34 @@ Item { } } - Img { - id: one - } + Component { + id: imgComp - Img { - id: two - } + CachingImage { + id: img - component Img: CachingImage { - id: img + anchors.fill: parent - function update(): void { - if (path === root.source) - root.current = this; - else - path = root.source; - } + opacity: 0 - anchors.fill: parent - - opacity: 0 - scale: Wallpapers.showPreview ? 1 : 0.8 - - onStatusChanged: { - if (status === Image.Ready) - root.current = this; - } - - states: State { - name: "visible" - when: root.current === img - - PropertyChanges { - img.opacity: 1 - img.scale: 1 + onStatusChanged: { + if (status === Image.Ready) + anim.start(); } - } - transitions: Transition { - Anim { - target: img - properties: "opacity,scale" + Anim on opacity { + id: anim + + type: Anim.SlowEffects + running: false + from: 0 + to: 1 + } + + Timer { + running: root.current !== img && root.current?.status === Image.Ready + interval: anim.duration + onTriggered: img.destroy() } } } diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index a2ed060e..db648a9c 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -102,7 +102,7 @@ ColumnLayout { } } - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Repeater { id: repeater diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 4fde197a..b111d1da 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -18,7 +18,7 @@ Item { readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name) readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) - readonly property int padding: Math.max(Tokens.padding.smaller, Config.border.thickness) + readonly property int padding: Math.max(Tokens.padding.small, Config.border.thickness) readonly property int contentWidth: Tokens.sizes.bar.innerWidth + padding * 2 readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered) @@ -37,7 +37,7 @@ Item { } clip: true - visible: width > 0 + visible: width > Config.border.thickness implicitWidth: fullscreen ? 0 : Config.border.thickness states: State { @@ -57,7 +57,6 @@ Item { Anim { target: root property: "implicitWidth" - type: Anim.DefaultSpatial } }, Transition { @@ -79,7 +78,7 @@ Item { anchors.bottom: parent.bottom anchors.right: parent.right - active: root.shouldBeVisible || root.visible + active: root.shouldBeVisible sourceComponent: Bar { width: root.contentWidth diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index f39aa4c3..8157e118 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -86,8 +86,7 @@ Item { id: metrics text: root.windowTitle - font.pointSize: root.Tokens.font.size.smaller - font.family: root.Tokens.font.family.mono + font: root.Tokens.font.body.builders.small.letterSpacing(1.4).build() elide: Qt.ElideRight elideWidth: root.maxHeight - icon.height @@ -100,9 +99,7 @@ Item { } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } component Title: StyledText { @@ -112,8 +109,7 @@ Item { anchors.top: icon.bottom anchors.topMargin: Tokens.spacing.small - font.pointSize: metrics.font.pointSize - font.family: metrics.font.family + font: metrics.font color: root.colour opacity: root.current === this ? 1 : 0 @@ -132,7 +128,9 @@ Item { height: implicitWidth Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index ffe599af..7e86ac47 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import QtQuick.Layouts import Caelestia.Config import qs.components import qs.services @@ -9,7 +10,8 @@ StyledRect { id: root readonly property color colour: Colours.palette.m3tertiary - readonly property int padding: Config.bar.clock.background ? Tokens.padding.normal : Tokens.padding.small + readonly property int padding: Config.bar.clock.background ? Tokens.padding.medium : Tokens.padding.extraSmall + readonly property var font: Tokens.font.body.builders.small.scale(1.1) implicitWidth: Tokens.sizes.bar.innerWidth implicitHeight: layout.implicitHeight + root.padding * 2 @@ -17,16 +19,15 @@ StyledRect { color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Tokens.rounding.full - Column { + ColumnLayout { id: layout anchors.centerIn: parent spacing: Tokens.spacing.small Loader { + Layout.alignment: Qt.AlignHCenter asynchronous: true - anchors.horizontalCenter: parent.horizontalCenter - active: Config.bar.clock.showIcon visible: active @@ -37,35 +38,69 @@ StyledRect { } StyledText { - anchors.horizontalCenter: parent.horizontalCenter - + Layout.alignment: Qt.AlignHCenter visible: Config.bar.clock.showDate horizontalAlignment: StyledText.AlignHCenter text: Time.format("ddd\nd") - font.pointSize: Tokens.font.size.smaller - font.family: Tokens.font.family.sans + font: Tokens.font.body.small color: root.colour } Rectangle { - anchors.horizontalCenter: parent.horizontalCenter + Layout.fillWidth: true visible: Config.bar.clock.showDate - height: visible ? 1 : 0 - - width: parent.width * 0.8 - color: root.colour - opacity: 0.2 + implicitHeight: 1 + color: Colours.palette.m3outlineVariant } StyledText { - anchors.horizontalCenter: parent.horizontalCenter - - horizontalAlignment: StyledText.AlignHCenter - text: Time.format(GlobalConfig.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") - font.pointSize: Tokens.font.size.smaller - font.family: Tokens.font.family.mono + Layout.alignment: Qt.AlignHCenter + text: Time.hourStr + font: { + const scale = text === "11" ? 1.15 : Math.min(1.05, Math.max(hourMetrics.width, minMetrics.width) / hourMetrics.width); + return root.font.width(scale * 100).letterSpacing(scale).build(); + } color: root.colour + + TextMetrics { + id: hourMetrics + + font: root.font.build() + text: Time.hourStr + } + } + + StyledText { + Layout.topMargin: -parent.spacing - 4 + Layout.alignment: Qt.AlignHCenter + text: Time.minuteStr + font: { + const scale = text === "11" ? 1.15 : Math.min(1.05, Math.max(hourMetrics.width, minMetrics.width) / minMetrics.width); + return root.font.width(scale * 100).letterSpacing(scale).build(); + } + color: root.colour + + TextMetrics { + id: minMetrics + + font: root.font.build() + text: Time.minuteStr + } + } + + Loader { + Layout.topMargin: -parent.spacing - 4 + Layout.alignment: Qt.AlignHCenter + asynchronous: true + active: GlobalConfig.services.useTwelveHourClock + visible: active + + sourceComponent: StyledText { + text: Time.amPmStr.toLowerCase() + font: Tokens.font.body.builders.small.scale(0.9).build() + color: root.colour + } } } } diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index 94d1a1c4..99314470 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -8,8 +8,8 @@ import qs.utils Item { id: root - implicitWidth: Math.round(Tokens.font.size.large * 1.2) - implicitHeight: Math.round(Tokens.font.size.large * 1.2) + implicitWidth: Math.round(Tokens.font.body.large.pointSize * 1.2) + implicitHeight: Math.round(Tokens.font.body.large.pointSize * 1.2) MouseArea { anchors.fill: parent @@ -30,8 +30,8 @@ Item { id: caelestiaLogo Logo { - implicitWidth: Math.round(Tokens.font.size.large * 1.6) - implicitHeight: Math.round(Tokens.font.size.large * 1.6) + implicitWidth: Math.round(Tokens.font.body.large.pointSize * 1.6) + implicitHeight: Math.round(Tokens.font.body.large.pointSize * 1.6) } } @@ -40,7 +40,7 @@ Item { ColouredIcon { source: SysInfo.osLogo - implicitSize: Math.round(Tokens.font.size.large * 1.2) + implicitSize: Math.round(Tokens.font.body.large.pointSize * 1.2) colour: Colours.palette.m3tertiary } } diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index 681a805d..ba810f8b 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -8,7 +8,7 @@ Item { required property DrawerVisibilities visibilities - implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 + implicitWidth: icon.implicitHeight + Tokens.padding.small implicitHeight: icon.implicitHeight StateLayer { @@ -16,7 +16,7 @@ Item { anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.small radius: Tokens.rounding.full onClicked: root.visibilities.session = !root.visibilities.session } @@ -29,7 +29,6 @@ Item { text: "power_settings_new" color: Colours.palette.m3error - font.bold: true - font.pointSize: Tokens.font.size.normal + fontStyle: Tokens.font.icon.builders.small.weight(Font.Bold).build() } } diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 900e5574..539a07f2 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -21,7 +21,7 @@ StyledRect { clip: true implicitWidth: Tokens.sizes.bar.innerWidth - implicitHeight: iconColumn.implicitHeight + Tokens.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) + implicitHeight: iconColumn.implicitHeight + Tokens.padding.medium * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) ColumnLayout { id: iconColumn @@ -29,9 +29,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.bottomMargin: Tokens.padding.normal + anchors.bottomMargin: Tokens.padding.medium - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 // Lock keys status WrappedLoader { @@ -57,7 +57,9 @@ StyledRect { color: root.colour Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -88,7 +90,9 @@ StyledRect { color: root.colour Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -136,7 +140,7 @@ StyledRect { animate: true text: Hypr.kbLayout color: root.colour - font.family: Tokens.font.family.mono + font: Tokens.font.mono.medium } } @@ -172,7 +176,7 @@ StyledRect { active: Config.bar.status.showBluetooth sourceComponent: ColumnLayout { - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 // Bluetooth icon MaterialIcon { @@ -245,15 +249,7 @@ StyledRect { return "rocket_launch"; return "balance"; } - - const perc = UPower.displayDevice.percentage; - const charging = [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state); - if (perc === 1) - return charging ? "battery_charging_full" : "battery_full"; - let level = Math.floor(perc * 7); - if (charging && (level === 4 || level === 1)) - level--; - return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; + return Icons.getBatteryIcon(UPower.displayDevice.percentage, [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state)); } color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? root.colour : Colours.palette.m3error fill: 1 diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 85a920c1..4545d554 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -14,7 +14,7 @@ StyledRect { readonly property alias items: items readonly property alias expandIcon: expandIcon - readonly property int padding: Config.bar.tray.background ? Tokens.padding.normal : Tokens.padding.small + readonly property int padding: Config.bar.tray.background ? Tokens.padding.medium : Tokens.padding.extraSmall readonly property int spacing: Config.bar.tray.background ? Tokens.spacing.small : 0 property bool expanded @@ -75,7 +75,9 @@ StyledRect { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -91,16 +93,16 @@ StyledRect { sourceComponent: Item { implicitWidth: expandIconInner.implicitWidth - implicitHeight: expandIconInner.implicitHeight - Tokens.padding.small * 2 + implicitHeight: expandIconInner.implicitHeight - Tokens.padding.small MaterialIcon { id: expandIconInner anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - anchors.bottomMargin: Config.bar.tray.background ? Tokens.padding.small : -Tokens.padding.small + anchors.bottomMargin: Config.bar.tray.background ? Tokens.padding.extraSmall : -Tokens.padding.extraSmall text: "expand_less" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large rotation: root.expanded ? 180 : 0 Behavior on rotation { @@ -115,8 +117,6 @@ StyledRect { } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } diff --git a/modules/bar/components/TrayItem.qml b/modules/bar/components/TrayItem.qml index fefb532c..5bc9f993 100644 --- a/modules/bar/components/TrayItem.qml +++ b/modules/bar/components/TrayItem.qml @@ -13,8 +13,8 @@ MouseArea { required property SystemTrayItem modelData acceptedButtons: Qt.LeftButton | Qt.RightButton - implicitWidth: Tokens.font.size.small * 2 - implicitHeight: Tokens.font.size.small * 2 + implicitWidth: Tokens.font.body.small.pointSize * 2 + implicitHeight: Tokens.font.body.small.pointSize * 2 onClicked: event => { if (event.button === Qt.LeftButton) diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index ebc0caf2..ad279498 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -44,7 +44,7 @@ StyledRect { clip: true y: offset + mask.y - implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small implicitHeight: size radius: Tokens.rounding.full color: Colours.palette.m3primary diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index 2bd3c8cb..d971940a 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -65,7 +65,7 @@ Item { anchors.horizontalCenter: root.horizontalCenter y: (start?.y ?? 0) - 1 - implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + 2 + implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small + 2 implicitHeight: start && end ? end.y + end.size - start.y + 2 : 0 color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index 3ed382e9..70476df8 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -18,7 +18,7 @@ Item { readonly property string activeSpecial: (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? "" layer.enabled: true - layer.effect: OpacityMask { + layer.effect: Mask { maskSource: mask } @@ -65,7 +65,9 @@ Item { opacity: view.contentY > 0 ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -76,10 +78,12 @@ Item { radius: Tokens.rounding.full implicitHeight: parent.height / 2 - opacity: view.contentY < view.contentHeight - parent.height + Tokens.padding.small ? 0 : 1 + opacity: view.contentY < view.contentHeight - parent.height + Tokens.padding.extraSmall ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -88,7 +92,7 @@ Item { id: view anchors.fill: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium interactive: false currentIndex: model.values.findIndex(w => w.name === root.activeSpecial) @@ -213,7 +217,7 @@ Item { drag.target: view.contentItem drag.axis: Drag.YAxis drag.maximumY: 0 - drag.minimumY: Math.min(0, view.height - view.contentHeight - Tokens.padding.small) + drag.minimumY: Math.min(0, view.height - view.contentHeight - Tokens.padding.extraSmall) onPressed: event => startY = event.y @@ -233,7 +237,7 @@ Item { id: ws required property HyprlandWorkspace modelData - readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Tokens.padding.small : 0) + readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Tokens.padding.extraSmall : 0) property int wsId property string icon property bool hasWindows @@ -284,7 +288,7 @@ Item { asynchronous: true Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small sourceComponent: ws.icon.length === 1 ? letterComp : iconComp diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index bd581aab..42ac844e 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -18,7 +18,7 @@ ColumnLayout { readonly property bool isWorkspace: true // Flag for finding workspace children // Unanimated prop for others to use as reference - readonly property int size: implicitHeight + (hasWindows ? Tokens.padding.small : 0) + readonly property int size: implicitHeight + (hasWindows ? Tokens.padding.extraSmall : 0) readonly property int ws: groupOffset + index + 1 readonly property bool isOccupied: occupied[ws] ?? false @@ -33,7 +33,7 @@ ColumnLayout { id: indicator Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small animate: true text: { @@ -52,6 +52,7 @@ ColumnLayout { } color: Config.bar.workspaces.occupiedBg || root.isOccupied || root.activeWsId === root.ws ? Colours.palette.m3onSurface : Colours.layer(Colours.palette.m3outlineVariant, 2) verticalAlignment: Qt.AlignVCenter + font.family: "Rubik" // Hard code rubik for now since google sans doesn't play well with certain unicode symbols apparently } Loader { diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index 2030bf80..2120aef9 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -28,7 +28,7 @@ StyledClippingRect { property real blur: onSpecial ? 1 : 0 implicitWidth: Tokens.sizes.bar.innerWidth - implicitHeight: layout.implicitHeight + Tokens.padding.small * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.small color: Colours.tPalette.m3surfaceContainer radius: Tokens.rounding.full @@ -51,7 +51,7 @@ StyledClippingRect { active: Config.bar.workspaces.occupiedBg anchors.fill: parent - anchors.margins: Tokens.padding.small + anchors.margins: Tokens.padding.extraSmall sourceComponent: OccupiedBg { workspaces: workspaces @@ -64,7 +64,7 @@ StyledClippingRect { id: layout anchors.centerIn: parent - spacing: Math.floor(Tokens.spacing.small / 2) + spacing: Math.floor(Tokens.spacing.extraSmall) Repeater { id: workspaces @@ -108,7 +108,9 @@ StyledClippingRect { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -118,7 +120,7 @@ StyledClippingRect { asynchronous: true anchors.fill: parent - anchors.margins: Tokens.padding.small + anchors.margins: Tokens.padding.extraSmall active: opacity > 0 @@ -134,7 +136,9 @@ StyledClippingRect { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index 12246645..dc2ef8cf 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -12,21 +12,21 @@ Item { required property PopoutState popouts - implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Tokens.padding.large * 2 + implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Tokens.padding.extraLargeIncreased implicitHeight: child.implicitHeight Column { id: child anchors.centerIn: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium RowLayout { id: detailsRow anchors.left: parent.left anchors.right: parent.right - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium IconImage { id: icon @@ -46,7 +46,7 @@ Item { StyledText { Layout.fillWidth: true text: Hypr.activeToplevel?.title ?? "" - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium elide: Text.ElideRight } @@ -59,13 +59,13 @@ Item { } Item { - implicitWidth: expandIcon.implicitHeight + Tokens.padding.small * 2 - implicitHeight: expandIcon.implicitHeight + Tokens.padding.small * 2 + implicitWidth: expandIcon.implicitHeight + Tokens.padding.small + implicitHeight: expandIcon.implicitHeight + Tokens.padding.small Layout.alignment: Qt.AlignVCenter StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: root.popouts.detachRequested("winfo") } @@ -77,14 +77,14 @@ Item { text: "chevron_right" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } ClippingWrapperRectangle { color: "transparent" - radius: Tokens.rounding.small + radius: Tokens.rounding.medium ScreencopyView { id: preview diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index e8dc9203..18983428 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -14,8 +14,8 @@ Item { required property PopoutState popouts - implicitWidth: layout.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: layout.implicitHeight + Tokens.padding.normal * 2 + implicitWidth: layout.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.medium * 2 ButtonGroup { id: sinks @@ -30,11 +30,11 @@ Item { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: qsTr("Output device") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } Repeater { @@ -53,9 +53,9 @@ Item { } StyledText { - Layout.topMargin: Tokens.spacing.smaller + Layout.topMargin: Tokens.spacing.medium text: qsTr("Input device") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } Repeater { @@ -72,15 +72,14 @@ Item { } StyledText { - Layout.topMargin: Tokens.spacing.smaller - Layout.bottomMargin: -Tokens.spacing.small / 2 + Layout.topMargin: Tokens.spacing.medium text: qsTr("Volume (%1)").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`) - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } CustomMouseArea { Layout.fillWidth: true - implicitHeight: Tokens.padding.normal * 3 + implicitHeight: Tokens.padding.medium * 3 onWheel: event => { if (event.angleDelta.y > 0) @@ -95,20 +94,16 @@ Item { implicitHeight: parent.implicitHeight value: Audio.volume - onMoved: Audio.setVolume(value) - - Behavior on value { - Anim {} - } + onInteraction: value => Audio.setVolume(value) } } IconTextButton { Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer - verticalPadding: Tokens.padding.small + verticalPadding: Tokens.padding.extraSmall text: qsTr("Open settings") icon: "settings" diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 93d2012b..3c3ced51 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -9,7 +9,7 @@ import qs.services Column { id: root - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium width: Tokens.sizes.bar.batteryWidth StyledText { @@ -45,11 +45,11 @@ Column { height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 sourceComponent: StyledRect { - implicitWidth: child.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: child.implicitHeight + Tokens.padding.smaller * 2 + implicitWidth: child.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: child.implicitHeight + Tokens.padding.large color: Colours.palette.m3error - radius: Tokens.rounding.normal + radius: Tokens.rounding.large Column { id: child @@ -72,8 +72,7 @@ Column { anchors.verticalCenter: parent.verticalCenter text: qsTr("Performance Degraded") color: Colours.palette.m3onError - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.mono.builders.medium.weight(Font.Medium).build() } MaterialIcon { @@ -109,8 +108,8 @@ Column { anchors.horizontalCenter: parent.horizontalCenter - implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Tokens.padding.normal * 2 + Tokens.spacing.large * 2 - implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Tokens.padding.small * 2 + implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Tokens.padding.medium * 2 + Tokens.spacing.largeIncreased * 2 + implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Tokens.padding.small color: Colours.tPalette.m3surfaceContainer radius: Tokens.rounding.full @@ -147,9 +146,7 @@ Column { ] transitions: Transition { - AnchorAnim { - type: AnchorAnim.Emphasized - } + AnchorAnim {} } } @@ -158,7 +155,7 @@ Column { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: Tokens.padding.small + anchors.leftMargin: Tokens.padding.extraSmall profile: PowerProfile.PowerSaver icon: "energy_savings_leaf" @@ -178,7 +175,7 @@ Column { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Tokens.padding.small + anchors.rightMargin: Tokens.padding.extraSmall profile: PowerProfile.Performance icon: "rocket_launch" @@ -199,8 +196,8 @@ Column { required property string icon required property int profile - implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 - implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 + implicitWidth: icon.implicitHeight + Tokens.padding.small + implicitHeight: icon.implicitHeight + Tokens.padding.small StateLayer { radius: Tokens.rounding.full @@ -214,12 +211,14 @@ Column { anchors.centerIn: parent text: parent.icon - font.pointSize: Tokens.font.size.large - color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + fontStyle: Tokens.font.icon.large + color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurfaceVariant fill: profiles.current === text ? 1 : 0 Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index baca115e..ebf21de3 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -19,10 +19,10 @@ ColumnLayout { spacing: Tokens.spacing.small StyledText { - Layout.topMargin: Tokens.padding.normal - Layout.rightMargin: Tokens.padding.small + Layout.topMargin: Tokens.padding.medium + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("Bluetooth") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } Toggle { @@ -47,7 +47,7 @@ ColumnLayout { StyledText { Layout.topMargin: Tokens.spacing.small - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall text: { const devices = Bluetooth.devices.values; // qmllint disable unresolved-type let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s"); @@ -57,7 +57,7 @@ ColumnLayout { return available; } color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } Repeater { @@ -72,7 +72,7 @@ ColumnLayout { readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall spacing: Tokens.spacing.small opacity: 0 @@ -84,7 +84,9 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -96,8 +98,8 @@ ColumnLayout { } StyledText { - Layout.leftMargin: Tokens.spacing.small / 2 - Layout.rightMargin: Tokens.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.extraSmall + Layout.rightMargin: Tokens.spacing.extraSmall Layout.fillWidth: true text: device.modelData.name elide: Text.ElideRight @@ -105,15 +107,15 @@ ColumnLayout { MaterialIcon { visible: device.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type - text: Icons.getBatteryIcon(device.modelData.batteryAvailable ? device.modelData.battery * 100 : -1) - color: device.modelData.battery < 0.2 ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant + text: device.modelData.batteryAvailable ? Icons.getBatteryIcon(device.modelData.battery) : "battery_alert" + color: device.modelData.batteryAvailable && device.modelData.battery < 0.2 ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant } StyledRect { id: connectBtn implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.small + implicitHeight: connectIcon.implicitHeight + Tokens.padding.extraSmall radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) // qmllint disable unresolved-type @@ -140,7 +142,9 @@ ColumnLayout { opacity: device.loading ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -169,10 +173,10 @@ ColumnLayout { IconTextButton { Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer - verticalPadding: Tokens.padding.small + verticalPadding: Tokens.padding.extraSmall text: qsTr("Open settings") icon: "settings" @@ -185,8 +189,8 @@ ColumnLayout { property alias toggle: toggle Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small - spacing: Tokens.spacing.normal + Layout.rightMargin: Tokens.padding.extraSmall + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml index ab80d2cc..99785219 100644 --- a/modules/bar/popouts/ClipWrapper.qml +++ b/modules/bar/popouts/ClipWrapper.qml @@ -33,9 +33,7 @@ Item { } Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on x { diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 42c5a766..a3d776bf 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -14,8 +14,8 @@ Item { readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Item current: currentPopout?.item ?? null - implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.large * 2 - implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.large * 2 + implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.extraLargeIncreased + implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.extraLargeIncreased Item { id: content @@ -172,7 +172,6 @@ Item { anchors.centerIn: parent opacity: 0 - scale: 0.8 active: false states: State { @@ -182,7 +181,6 @@ Item { PropertyChanges { popout.active: true popout.opacity: 1 - popout.scale: 1 } } @@ -193,11 +191,10 @@ Item { SequentialAnimation { Anim { - properties: "opacity,scale" - type: Anim.StandardSmall + property: "opacity" + type: Anim.DefaultEffects } PropertyAction { - target: popout property: "active" } } @@ -208,11 +205,11 @@ Item { SequentialAnimation { PropertyAction { - target: popout property: "active" } Anim { - properties: "opacity,scale" + property: "opacity" + type: Anim.SlowEffects } } } diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 63b69b57..f8d8247d 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -26,10 +26,10 @@ ColumnLayout { StyledText { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Tokens.padding.normal : 0 - Layout.rightMargin: Tokens.padding.small + Layout.topMargin: visible ? Tokens.padding.medium : 0 + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("Wireless") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } Toggle { @@ -44,10 +44,10 @@ ColumnLayout { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Tokens.spacing.small : 0 - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("%1 networks available").arg(Nmcli.networks.length) // qmllint disable missing-property color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } Repeater { @@ -70,7 +70,7 @@ ColumnLayout { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall spacing: Tokens.spacing.small opacity: 0 @@ -82,7 +82,9 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -97,22 +99,22 @@ ColumnLayout { MaterialIcon { visible: networkItem.modelData.isSecure text: "lock" - font.pointSize: Tokens.font.size.small + fontStyle: Tokens.font.icon.small } StyledText { - Layout.leftMargin: Tokens.spacing.small / 2 - Layout.rightMargin: Tokens.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.extraSmall + Layout.rightMargin: Tokens.spacing.extraSmall Layout.fillWidth: true text: networkItem.modelData.ssid elide: Text.ElideRight - font.weight: networkItem.modelData.active ? 500 : 400 + font: Tokens.font.body.builders.medium.weight(networkItem.modelData.active ? Font.Medium : Font.Normal).build() color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurface } StyledRect { implicitWidth: implicitHeight - implicitHeight: wirelessConnectIcon.implicitHeight + Tokens.padding.small + implicitHeight: wirelessConnectIcon.implicitHeight + Tokens.padding.extraSmall radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0) @@ -155,7 +157,9 @@ ColumnLayout { opacity: networkItem.loading ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -167,7 +171,7 @@ ColumnLayout { Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Tokens.spacing.small : 0 Layout.fillWidth: true - implicitHeight: rescanBtn.implicitHeight + Tokens.padding.small * 2 + implicitHeight: rescanBtn.implicitHeight + Tokens.padding.small radius: Tokens.rounding.full color: Colours.palette.m3primaryContainer @@ -201,15 +205,17 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } CircularIndicator { anchors.centerIn: parent - strokeWidth: Tokens.padding.small / 2 + strokeWidth: Tokens.padding.extraSmall / 2 bgColour: "transparent" - implicitSize: parent.implicitHeight - Tokens.padding.smaller * 2 + implicitSize: parent.implicitHeight - Tokens.padding.large running: Nmcli.scanning } } @@ -218,20 +224,20 @@ ColumnLayout { StyledText { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Tokens.padding.normal : 0 - Layout.rightMargin: Tokens.padding.small + Layout.topMargin: visible ? Tokens.padding.medium : 0 + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("Ethernet") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } StyledText { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Tokens.spacing.small : 0 - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } Repeater { @@ -253,7 +259,7 @@ ColumnLayout { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall spacing: Tokens.spacing.small opacity: 0 @@ -265,7 +271,9 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -278,18 +286,18 @@ ColumnLayout { } StyledText { - Layout.leftMargin: Tokens.spacing.small / 2 - Layout.rightMargin: Tokens.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.extraSmall + Layout.rightMargin: Tokens.spacing.extraSmall Layout.fillWidth: true text: ethernetItem.modelData.interface || qsTr("Unknown") elide: Text.ElideRight - font.weight: ethernetItem.modelData.connected ? 500 : 400 + font: Tokens.font.body.builders.medium.weight(ethernetItem.modelData.connected ? Font.Medium : Font.Normal).build() color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface } StyledRect { implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.small + implicitHeight: connectIcon.implicitHeight + Tokens.padding.extraSmall radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) @@ -323,7 +331,9 @@ ColumnLayout { opacity: ethernetItem.loading ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -371,8 +381,8 @@ ColumnLayout { property alias toggle: toggle Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small - spacing: Tokens.spacing.normal + Layout.rightMargin: Tokens.padding.extraSmall + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 7975d1bf..c4b8bf2c 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -45,7 +45,7 @@ StackView { property bool isSubMenu property bool shown - padding: Tokens.padding.smaller + padding: Tokens.padding.small spacing: Tokens.spacing.small opacity: shown ? 1 : 0 @@ -57,7 +57,9 @@ StackView { StackView.onRemoved: destroy() Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -97,9 +99,9 @@ StackView { implicitHeight: label.implicitHeight StateLayer { - anchors.margins: -Tokens.padding.small / 2 - anchors.leftMargin: -Tokens.padding.smaller - anchors.rightMargin: -Tokens.padding.smaller + anchors.margins: -Tokens.padding.extraSmall / 2 + anchors.leftMargin: -Tokens.padding.small + anchors.rightMargin: -Tokens.padding.small radius: item.radius disabled: !item.modelData.enabled @@ -138,7 +140,7 @@ StackView { id: label anchors.left: icon.right - anchors.leftMargin: icon.active ? Tokens.spacing.smaller : 0 + anchors.leftMargin: icon.active ? Tokens.spacing.medium : 0 text: labelMetrics.elidedText color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline @@ -148,11 +150,10 @@ StackView { id: labelMetrics text: item.modelData.text - font.pointSize: label.font.pointSize - font.family: label.font.family + font: label.font elide: Text.ElideRight - elideWidth: root.Tokens.sizes.bar.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + root.Tokens.spacing.normal : 0) + elideWidth: root.Tokens.sizes.bar.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + root.Tokens.spacing.medium : 0) } Loader { @@ -180,7 +181,7 @@ StackView { sourceComponent: Item { implicitWidth: back.implicitWidth - implicitHeight: back.implicitHeight + Tokens.spacing.small / 2 + implicitHeight: back.implicitHeight + Tokens.spacing.extraSmall Item { anchors.bottom: parent.bottom @@ -189,9 +190,9 @@ StackView { StyledRect { anchors.fill: parent - anchors.margins: -Tokens.padding.small / 2 - anchors.leftMargin: -Tokens.padding.smaller - anchors.rightMargin: -Tokens.padding.smaller * 2 + anchors.margins: -Tokens.padding.extraSmall / 2 + anchors.leftMargin: -Tokens.padding.small + anchors.rightMargin: -Tokens.padding.large radius: Tokens.rounding.full color: Colours.palette.m3secondaryContainer diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index f6a635fe..40d159df 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -69,9 +69,9 @@ ColumnLayout { } } - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium implicitWidth: 400 - implicitHeight: content.implicitHeight + Tokens.padding.large * 2 + implicitHeight: content.implicitHeight + Tokens.padding.extraLargeIncreased visible: shouldBeVisible || isClosing enabled: shouldBeVisible && !isClosing focus: enabled @@ -128,8 +128,8 @@ ColumnLayout { StyledRect { Layout.fillWidth: true Layout.preferredWidth: 400 - implicitHeight: content.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: content.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer visible: root.shouldBeVisible || root.isClosing opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 @@ -137,7 +137,9 @@ ColumnLayout { Keys.onEscapePressed: root.closeDialog() Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -153,6 +155,7 @@ ColumnLayout { } Anim { + type: Anim.DefaultEffects target: parent property: "opacity" to: 0 @@ -172,19 +175,18 @@ ColumnLayout { anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "lock" - font.pointSize: Tokens.font.size.extraLarge * 2 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).build() } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enter password") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { @@ -201,7 +203,7 @@ ColumnLayout { return qsTr("Network: Unknown"); } color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } Timer { @@ -249,10 +251,9 @@ ColumnLayout { return ""; } color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - font.weight: 400 + font: Tokens.font.body.builders.small.weight(Font.Normal).build() wrapMode: Text.WordWrap - Layout.maximumWidth: parent.width - Tokens.padding.large * 2 + Layout.maximumWidth: parent.width - Tokens.padding.extraLargeIncreased } FocusScope { @@ -261,9 +262,9 @@ ColumnLayout { property string passwordBuffer: "" objectName: "passwordContainer" - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased Layout.fillWidth: true - implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) + implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.medium * 2) focus: true activeFocusOnTab: true @@ -336,7 +337,7 @@ ColumnLayout { StyledRect { anchors.fill: parent - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) border.color: { @@ -365,7 +366,7 @@ ColumnLayout { StateLayer { hoverEnabled: false cursorShape: Qt.IBeamCursor - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: passwordContainer.forceActiveFocus() } @@ -375,12 +376,13 @@ ColumnLayout { anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - font.family: Tokens.font.family.mono + font: Tokens.font.mono.medium opacity: passwordContainer.passwordBuffer ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -391,10 +393,10 @@ ColumnLayout { anchors.centerIn: parent implicitWidth: fullWidth - implicitHeight: Tokens.font.size.normal + implicitHeight: Tokens.font.body.medium.pointSize orientation: Qt.Horizontal - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall interactive: false model: ScriptModel { @@ -408,7 +410,7 @@ ColumnLayout { implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface - radius: Tokens.rounding.small / 2 + radius: Tokens.rounding.medium / 2 opacity: 0 scale: 0 @@ -428,6 +430,7 @@ ColumnLayout { } ParallelAnimation { Anim { + type: Anim.DefaultEffects target: ch property: "opacity" to: 0 @@ -446,7 +449,9 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -463,15 +468,15 @@ ColumnLayout { } RowLayout { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium TextButton { id: cancelButton Layout.fillWidth: true - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + Layout.minimumHeight: Tokens.font.body.medium.pointSize + Tokens.padding.medium * 2 inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") @@ -486,7 +491,7 @@ ColumnLayout { property bool hasError: false Layout.fillWidth: true - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + Layout.minimumHeight: Tokens.font.body.medium.pointSize + Tokens.padding.medium * 2 inactiveColour: Colours.palette.m3primary inactiveOnColour: Colours.palette.m3onPrimary text: qsTr("Connect") diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 7a66d3b9..97589f9a 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -7,7 +7,7 @@ import Quickshell.Wayland import Caelestia.Config import qs.components import qs.services -import qs.modules.controlcenter +import qs.modules.nexus import qs.modules.windowinfo Item { @@ -18,7 +18,7 @@ Item { readonly property alias content: content readonly property alias winfo: winfo - readonly property alias controlCenter: controlCenter + readonly property alias nexus: nexus readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight @@ -128,15 +128,24 @@ Item { } Comp { - id: controlCenter + id: nexus shouldBeActive: root.detachedMode === "any" anchors.centerIn: parent - sourceComponent: ControlCenter { - screen: root.screen - active: root.queuedMode - onClose: root.close() + sourceComponent: StyledClippingRect { + radius: Tokens.rounding.extraLarge + implicitWidth: nexusInner.implicitWidth + implicitHeight: nexusInner.implicitHeight + + Nexus { + id: nexusInner + + anchors.fill: parent + nState.screen: root.screen + nState.currentPageIdx: ["appearance", "network", "bluetooth", "audio"].indexOf(root.queuedMode) + onClose: root.close() + } } } @@ -185,6 +194,7 @@ Item { property: "active" } Anim { + type: Anim.DefaultEffects property: "opacity" } } @@ -195,6 +205,7 @@ Item { SequentialAnimation { Anim { + type: Anim.DefaultEffects property: "opacity" } PropertyAction { diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index d8f9a462..abbbae09 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -24,10 +24,10 @@ ColumnLayout { } StyledText { - Layout.topMargin: Tokens.padding.normal - Layout.rightMargin: Tokens.padding.small + Layout.topMargin: Tokens.padding.medium + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("Keyboard Layouts") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } ListView { @@ -36,7 +36,7 @@ ColumnLayout { model: kb.visibleModel Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall Layout.topMargin: Tokens.spacing.small clip: true @@ -88,7 +88,7 @@ ColumnLayout { readonly property bool isDisabled: layoutIndex > 3 width: list.width - height: Math.max(36, rowText.implicitHeight + Tokens.padding.small * 2) + height: Math.max(36, rowText.implicitHeight + Tokens.padding.small) ToolTip.visible: isDisabled && layer.containsMouse ToolTip.text: "XKB limitation: maximum 4 layouts allowed" @@ -114,8 +114,8 @@ ColumnLayout { anchors.verticalCenter: layer.verticalCenter anchors.left: layer.left anchors.right: layer.right - anchors.leftMargin: Tokens.padding.small - anchors.rightMargin: Tokens.padding.small + anchors.leftMargin: Tokens.padding.extraSmall + anchors.rightMargin: Tokens.padding.extraSmall text: kbDelegate.label elide: Text.ElideRight opacity: kbDelegate.isDisabled ? 0.4 : 1.0 @@ -126,7 +126,7 @@ ColumnLayout { Rectangle { visible: kb.activeLabel.length > 0 Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall Layout.topMargin: Tokens.spacing.small implicitHeight: 1 @@ -139,7 +139,7 @@ ColumnLayout { visible: kb.activeLabel.length > 0 Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall Layout.topMargin: Tokens.spacing.small spacing: Tokens.spacing.small @@ -155,7 +155,7 @@ ColumnLayout { Layout.fillWidth: true text: kb.activeLabel elide: Text.ElideRight - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() color: Colours.palette.m3primary } diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index f62d0b98..1cd9243e 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -10,25 +10,25 @@ import Caelestia.Config Item { id: model - property alias visibleModel: _visibleModel + property alias visibleModel: visibleModel property string activeLabel: "" property int activeIndex: -1 property var _xkbMap: ({}) property bool _notifiedLimit: false function start() { - _xkbXmlBase.running = true; - _getKbLayoutOpt.running = true; + xkbXmlBase.running = true; + getKbLayoutOpt.running = true; } function refresh() { _notifiedLimit = false; - _getKbLayoutOpt.running = true; + getKbLayoutOpt.running = true; } function switchTo(idx) { - _switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)]; - _switchProc.running = true; + switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)]; + switchProc.running = true; } function _buildXmlMap(xml) { @@ -50,19 +50,19 @@ Item { _xkbMap = map; - if (_layoutsModel.count > 0) { + if (layoutsModel.count > 0) { const tmp = []; - for (let i = 0; i < _layoutsModel.count; i++) { - const it = _layoutsModel.get(i); + for (let i = 0; i < layoutsModel.count; i++) { + const it = layoutsModel.get(i); tmp.push({ layoutIndex: it.layoutIndex, token: it.token, label: _pretty(it.token) }); } - _layoutsModel.clear(); - tmp.forEach(t => _layoutsModel.append(t)); - _fetchActiveLayouts.running = true; + layoutsModel.clear(); + tmp.forEach(t => layoutsModel.append(t)); + fetchActiveLayouts.running = true; } } @@ -78,7 +78,7 @@ Item { function _setLayouts(raw) { const parts = raw.split(",").map(s => s.trim()).filter(Boolean); - _layoutsModel.clear(); + layoutsModel.clear(); const seen = new Set(); let idx = 0; @@ -87,7 +87,7 @@ Item { if (seen.has(p)) continue; seen.add(p); - _layoutsModel.append({ + layoutsModel.append({ layoutIndex: idx, token: p, label: _pretty(p) @@ -97,19 +97,19 @@ Item { } function _rebuildVisible() { - _visibleModel.clear(); + visibleModel.clear(); let arr = []; - for (let i = 0; i < _layoutsModel.count; i++) - arr.push(_layoutsModel.get(i)); + for (let i = 0; i < layoutsModel.count; i++) + arr.push(layoutsModel.get(i)); arr = arr.filter(i => i.layoutIndex !== activeIndex); - arr.forEach(i => _visibleModel.append(i)); + arr.forEach(i => visibleModel.append(i)); if (!GlobalConfig.utilities.toasts.kbLimit) return; - if (_layoutsModel.count > 4) { + if (layoutsModel.count > 4) { Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); } } @@ -124,26 +124,26 @@ Item { visible: false ListModel { - id: _visibleModel + id: visibleModel } ListModel { - id: _layoutsModel + id: layoutsModel } Process { - id: _xkbXmlBase + id: xkbXmlBase command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] stdout: StdioCollector { onStreamFinished: model._buildXmlMap(text) } - onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) // qmllint disable missing-property - _xkbXmlEvdev.running = true + onRunningChanged: if (!running && (typeof xkbXmlBase.exitCode !== "undefined") && xkbXmlBase.exitCode !== 0) // qmllint disable missing-property + xkbXmlEvdev.running = true } Process { - id: _xkbXmlEvdev + id: xkbXmlEvdev command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] stdout: StdioCollector { @@ -152,7 +152,7 @@ Item { } Process { - id: _getKbLayoutOpt + id: getKbLayoutOpt command: ["hyprctl", "-j", "getoption", "input:kb_layout"] stdout: StdioCollector { @@ -162,17 +162,17 @@ Item { const raw = (j?.str || j?.value || "").toString().trim(); if (raw.length) { model._setLayouts(raw); - _fetchActiveLayouts.running = true; + fetchActiveLayouts.running = true; return; } } catch (e) {} - _fetchLayoutsFromDevices.running = true; + fetchLayoutsFromDevices.running = true; } } } Process { - id: _fetchLayoutsFromDevices + id: fetchLayoutsFromDevices command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { @@ -184,13 +184,13 @@ Item { if (raw.length) model._setLayouts(raw); } catch (e) {} - _fetchActiveLayouts.running = true; + fetchActiveLayouts.running = true; } } } Process { - id: _fetchActiveLayouts + id: fetchActiveLayouts command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { @@ -201,7 +201,7 @@ Item { const idx = kb?.active_layout_index ?? -1; model.activeIndex = idx >= 0 ? idx : -1; - model.activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; + model.activeLabel = (idx >= 0 && idx < layoutsModel.count) ? layoutsModel.get(idx).label : ""; } catch (e) { model.activeIndex = -1; model.activeLabel = ""; @@ -213,9 +213,9 @@ Item { } Process { - id: _switchProc + id: switchProc onRunningChanged: if (!running) - _fetchActiveLayouts.running = true + fetchActiveLayouts.running = true } } diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml deleted file mode 100644 index c8b435ca..00000000 --- a/modules/controlcenter/ControlCenter.qml +++ /dev/null @@ -1,99 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Quickshell -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.services - -Item { - id: root - - required property ShellScreen screen - readonly property int rounding: floating ? 0 : Tokens.rounding.large - - property alias floating: session.floating - property alias active: session.active - property alias navExpanded: session.navExpanded - - readonly property bool initialOpeningComplete: panes.initialOpeningComplete - readonly property Session session: Session { - id: session - - root: root - } - - signal close - - implicitWidth: implicitHeight * Tokens.sizes.controlCenter.ratio - implicitHeight: screen.height * Tokens.sizes.controlCenter.heightMult - - GridLayout { - anchors.fill: parent - - rowSpacing: 0 - columnSpacing: 0 - rows: root.floating ? 2 : 1 - columns: 2 - - Loader { - Layout.fillWidth: true - Layout.columnSpan: 2 - - asynchronous: true - active: root.floating - visible: active - - sourceComponent: WindowTitle { - screen: root.screen - session: root.session - } - } - - StyledRect { - Layout.fillHeight: true - - topLeftRadius: root.rounding - bottomLeftRadius: root.rounding - implicitWidth: navRail.implicitWidth - color: Colours.tPalette.m3surfaceContainer - - CustomMouseArea { - function onWheel(event: WheelEvent): void { - // Prevent tab switching during initial opening animation to avoid blank pages - if (!panes.initialOpeningComplete) { - return; - } - - if (event.angleDelta.y < 0) - root.session.activeIndex = Math.min(root.session.activeIndex + 1, root.session.panes.length - 1); - else if (event.angleDelta.y > 0) - root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0); - } - - anchors.fill: parent - } - - NavRail { - id: navRail - - screen: root.screen - session: root.session - initialOpeningComplete: root.initialOpeningComplete - } - } - - Panes { - id: panes - - Layout.fillWidth: true - Layout.fillHeight: true - - topRightRadius: root.rounding - bottomRightRadius: root.rounding - session: root.session - } - } -} diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml deleted file mode 100644 index a126c800..00000000 --- a/modules/controlcenter/NavRail.qml +++ /dev/null @@ -1,230 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Quickshell -import Caelestia.Config -import qs.components -import qs.services -import qs.modules.controlcenter - -Item { - id: root - - required property ShellScreen screen - required property Session session - required property bool initialOpeningComplete - - implicitWidth: layout.implicitWidth + Tokens.padding.larger * 4 - implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 - - ColumnLayout { - id: layout - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.padding.larger * 2 - spacing: Tokens.spacing.normal - - states: State { - name: "expanded" - when: root.session.navExpanded - - PropertyChanges { - layout.spacing: root.Tokens.spacing.small - } - } - - transitions: Transition { - Anim { - properties: "spacing" - } - } - - Loader { - Layout.topMargin: Tokens.spacing.large - asynchronous: true - active: !root.session.floating - visible: active - - sourceComponent: StyledRect { - readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2 - - implicitWidth: nonAnimWidth - implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Tokens.padding.normal * 2 : nonAnimWidth - - color: Colours.palette.m3primaryContainer - radius: Tokens.rounding.small - - StateLayer { - id: normalWinState - - onClicked: { - root.session.root.close(); - WindowFactory.create(null, { - active: root.session.active, - navExpanded: root.session.navExpanded - }); - } - - color: Colours.palette.m3onPrimaryContainer - } - - MaterialIcon { - id: normalWinIcon - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.padding.large - - text: "select_window" - color: Colours.palette.m3onPrimaryContainer - font.pointSize: Tokens.font.size.large - fill: 1 - } - - StyledText { - id: normalWinLabel - - anchors.left: normalWinIcon.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.spacing.normal - - text: qsTr("Float window") - color: Colours.palette.m3onPrimaryContainer - opacity: root.session.navExpanded ? 1 : 0 - - Behavior on opacity { - Anim { - type: Anim.StandardSmall - } - } - } - - Behavior on implicitWidth { - Anim { - type: Anim.DefaultSpatial - } - } - - Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } - } - } - } - - Repeater { - model: PaneRegistry.count - - NavItem { - required property int index - - Layout.topMargin: index === 0 ? Tokens.spacing.large * 2 : 0 - icon: PaneRegistry.getByIndex(index).icon - label: PaneRegistry.getByIndex(index).label - } - } - } - - component NavItem: Item { - id: item - - required property string icon - required property string label - readonly property bool active: root.session.active === label - - implicitWidth: background.implicitWidth - implicitHeight: background.implicitHeight + smallLabel.implicitHeight + smallLabel.anchors.topMargin - - states: State { - name: "expanded" - when: root.session.navExpanded - - PropertyChanges { - expandedLabel.opacity: 1 - smallLabel.opacity: 0 - background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth - background.implicitHeight: icon.implicitHeight + root.Tokens.padding.normal * 2 - item.implicitHeight: background.implicitHeight - } - } - - transitions: Transition { - Anim { - property: "opacity" - type: Anim.StandardSmall - } - - Anim { - properties: "implicitWidth,implicitHeight" - type: Anim.DefaultSpatial - } - } - - StyledRect { - id: background - - radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0) - - implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 - implicitHeight: icon.implicitHeight + Tokens.padding.small - - StateLayer { - onClicked: { - // Prevent tab switching during initial opening animation to avoid blank pages - if (!root.initialOpeningComplete) { - return; - } - root.session.active = item.label; - } - - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - } - - MaterialIcon { - id: icon - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.padding.large - - text: item.icon - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.large - fill: item.active ? 1 : 0 - - Behavior on fill { - Anim {} - } - } - - StyledText { - id: expandedLabel - - anchors.left: icon.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.spacing.normal - - opacity: 0 - text: item.label - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.capitalization: Font.Capitalize - } - - StyledText { - id: smallLabel - - anchors.horizontalCenter: icon.horizontalCenter - anchors.top: icon.bottom - anchors.topMargin: Tokens.spacing.small / 2 - - text: item.label - font.pointSize: Tokens.font.size.small - font.capitalization: Font.Capitalize - } - } - } -} diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml deleted file mode 100644 index 4d85969b..00000000 --- a/modules/controlcenter/PaneRegistry.qml +++ /dev/null @@ -1,98 +0,0 @@ -pragma Singleton - -import QtQuick - -QtObject { - id: root - - readonly property list panes: [ - QtObject { - readonly property string id: "network" - readonly property string label: "network" - readonly property string icon: "router" - readonly property string component: "network/NetworkingPane.qml" - }, - QtObject { - readonly property string id: "bluetooth" - readonly property string label: "bluetooth" - readonly property string icon: "settings_bluetooth" - readonly property string component: "bluetooth/BtPane.qml" - }, - QtObject { - readonly property string id: "audio" - readonly property string label: "audio" - readonly property string icon: "volume_up" - readonly property string component: "audio/AudioPane.qml" - }, - QtObject { - readonly property string id: "appearance" - readonly property string label: "appearance" - readonly property string icon: "palette" - readonly property string component: "appearance/AppearancePane.qml" - }, - QtObject { - readonly property string id: "taskbar" - readonly property string label: "taskbar" - readonly property string icon: "task_alt" - readonly property string component: "taskbar/TaskbarPane.qml" - }, - QtObject { - readonly property string id: "notifications" - readonly property string label: "notifications" - readonly property string icon: "notifications" - readonly property string component: "notifications/NotificationsPane.qml" - }, - QtObject { - readonly property string id: "launcher" - readonly property string label: "launcher" - readonly property string icon: "apps" - readonly property string component: "launcher/LauncherPane.qml" - }, - QtObject { - readonly property string id: "dashboard" - readonly property string label: "dashboard" - readonly property string icon: "dashboard" - readonly property string component: "dashboard/DashboardPane.qml" - } - ] - - readonly property int count: panes.length - - readonly property var labels: { - const result = []; - for (let i = 0; i < panes.length; i++) { - result.push(panes[i].label); - } - return result; - } - - function getByIndex(index: int): var { - if (index >= 0 && index < panes.length) { - return panes[index]; - } - return null; - } - - function getIndexByLabel(label: string): int { - for (let i = 0; i < panes.length; i++) { - if (panes[i].label === label) { - return i; - } - } - return -1; - } - - function getByLabel(label: string): var { - const index = getIndexByLabel(label); - return getByIndex(index); - } - - function getById(id: string): var { - for (let i = 0; i < panes.length; i++) { - if (panes[i].id === id) { - return panes[i]; - } - } - return null; - } -} diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml deleted file mode 100644 index c3a8e21b..00000000 --- a/modules/controlcenter/Panes.qml +++ /dev/null @@ -1,182 +0,0 @@ -pragma ComponentBehavior: Bound - -import "bluetooth" -import "network" -import "audio" -import "appearance" -import "taskbar" -import "notifications" -import "launcher" -import "dashboard" -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets -import Caelestia.Config -import qs.components -import qs.services -import qs.modules.controlcenter - -ClippingRectangle { - id: root - - required property Session session - - readonly property bool initialOpeningComplete: layout.initialOpeningComplete - - color: "transparent" - clip: true - focus: false - activeFocusOnTab: false - - MouseArea { - anchors.fill: parent - z: -1 - onPressed: function (mouse) { - root.focus = true; - mouse.accepted = false; - } - } - - Connections { - function onActiveIndexChanged(): void { - root.focus = true; - } - - target: root.session - } - - ColumnLayout { - id: layout - - property bool animationComplete: true - property bool initialOpeningComplete: false - - spacing: 0 - y: -root.session.activeIndex * root.height - clip: true - - Timer { - id: animationDelayTimer - - interval: Tokens.anim.durations.normal - onTriggered: { - layout.animationComplete = true; - } - } - - Timer { - id: initialOpeningTimer - - interval: Tokens.anim.durations.large - running: true - onTriggered: { - layout.initialOpeningComplete = true; - } - } - - Repeater { - model: PaneRegistry.count - - Pane { - required property int index - - paneIndex: index - componentPath: PaneRegistry.getByIndex(index).component - } - } - - Behavior on y { - Anim {} - } - - Connections { - function onActiveIndexChanged(): void { - layout.animationComplete = false; - animationDelayTimer.restart(); - } - - target: root.session - } - } - - component Pane: Item { - id: pane - - required property int paneIndex - required property string componentPath - property bool hasBeenLoaded: false - - function updateActive(): void { - const diff = Math.abs(root.session.activeIndex - pane.paneIndex); - const isActivePane = diff === 0; - let shouldBeActive = false; - - if (!layout.initialOpeningComplete) { - shouldBeActive = isActivePane; - } else { - if (diff <= 1) { - shouldBeActive = true; - } else if (pane.hasBeenLoaded) { - shouldBeActive = true; - } else { - shouldBeActive = layout.animationComplete; - } - } - - loader.active = shouldBeActive; - } - - implicitWidth: root.width - implicitHeight: root.height - - Loader { - id: loader - - anchors.fill: parent - asynchronous: true - clip: false - active: false - - Component.onCompleted: { - Qt.callLater(pane.updateActive); - } - - onActiveChanged: { - if (active && !pane.hasBeenLoaded) { - pane.hasBeenLoaded = true; - } - - if (active && !item) { - loader.setSource(pane.componentPath, { - "session": root.session - }); - } - } - - onItemChanged: { - if (item) { - pane.hasBeenLoaded = true; - } - } - } - - Connections { - function onActiveIndexChanged(): void { - pane.updateActive(); - } - - target: root.session - } - - Connections { - function onInitialOpeningCompleteChanged(): void { - pane.updateActive(); - } - function onAnimationCompleteChanged(): void { - pane.updateActive(); - } - - target: layout - } - } -} diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml deleted file mode 100644 index e3839681..00000000 --- a/modules/controlcenter/Session.qml +++ /dev/null @@ -1,23 +0,0 @@ -import "./state" -import QtQuick -import qs.modules.controlcenter - -QtObject { - readonly property list panes: PaneRegistry.labels - - required property var root - property bool floating: false - property string active: "network" - property int activeIndex: 0 - property bool navExpanded: false - - readonly property BluetoothState bt: BluetoothState {} - readonly property NetworkState network: NetworkState {} - readonly property EthernetState ethernet: EthernetState {} - readonly property LauncherState launcher: LauncherState {} - readonly property VpnState vpn: VpnState {} - - onActiveChanged: activeIndex = Math.max(0, panes.indexOf(active)) - onActiveIndexChanged: if (panes[activeIndex]) - active = panes[activeIndex] -} diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml deleted file mode 100644 index dc0dc4a0..00000000 --- a/modules/controlcenter/WindowFactory.qml +++ /dev/null @@ -1,59 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import qs.components -import qs.services - -Singleton { - id: root - - function create(parent: Item, props: var): void { - controlCenter.createObject(parent ?? dummy, props); - } - - QtObject { - id: dummy - } - - Component { - id: controlCenter - - FloatingWindow { - id: win - - property alias active: cc.active - property alias navExpanded: cc.navExpanded - - color: Colours.tPalette.m3surface - - onVisibleChanged: { - if (!visible) - destroy(); - } - - implicitWidth: cc.implicitWidth - implicitHeight: cc.implicitHeight - - minimumSize.width: implicitWidth - minimumSize.height: implicitHeight - maximumSize.width: implicitWidth - maximumSize.height: implicitHeight - - title: qsTr("Caelestia Settings - %1").arg(cc.active.slice(0, 1).toUpperCase() + cc.active.slice(1)) - - ControlCenter { - id: cc - - anchors.fill: parent - screen: win.screen - onClose: win.destroy() - floating: true - } - - Behavior on color { - CAnim {} - } - } - } -} diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml deleted file mode 100644 index 8f66968b..00000000 --- a/modules/controlcenter/WindowTitle.qml +++ /dev/null @@ -1,51 +0,0 @@ -import QtQuick -import Quickshell -import Caelestia.Config -import qs.components -import qs.services - -StyledRect { - id: root - - required property ShellScreen screen - required property Session session - - implicitHeight: text.implicitHeight + Tokens.padding.normal - color: Colours.tPalette.m3surfaceContainer - - StyledText { - id: text - - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - - text: qsTr("Caelestia Settings - %1").arg(root.session.active) - font.capitalization: Font.Capitalize - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - Item { - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Tokens.padding.normal - - implicitWidth: implicitHeight - implicitHeight: closeIcon.implicitHeight + Tokens.padding.small - - StateLayer { - onClicked: { - QsWindow.window.destroy(); - } - - radius: Tokens.rounding.full - } - - MaterialIcon { - id: closeIcon - - anchors.centerIn: parent - text: "close" - } - } -} diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml deleted file mode 100644 index a1e0e42c..00000000 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ /dev/null @@ -1,267 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import "./sections" -import "../../launcher/services" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia.Config -import Caelestia.Models -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.components.images -import qs.services -import qs.utils - -Item { - id: root - - required property Session session - - property real animDurationsScale: GlobalConfig.appearance.anim.durations.scale ?? 1 - property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded" - property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF" - property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik" - property real fontSizeScale: Config.appearance.font.size.scale ?? 1 - property real paddingScale: Config.appearance.padding.scale ?? 1 - property real roundingScale: Config.appearance.rounding.scale ?? 1 - property real spacingScale: Config.appearance.spacing.scale ?? 1 - property bool transparencyEnabled: GlobalConfig.appearance.transparency.enabled ?? false - property real transparencyBase: GlobalConfig.appearance.transparency.base ?? 0.85 - property real transparencyLayers: GlobalConfig.appearance.transparency.layers ?? 0.4 - property real borderRounding: Config.border.rounding ?? 1 - property real borderThickness: Config.border.thickness ?? 1 - - property bool desktopClockEnabled: Config.background.desktopClock.enabled ?? false - property real desktopClockScale: Config.background.desktopClock.scale ?? 1 - property string desktopClockPosition: Config.background.desktopClock.position ?? "bottom-right" - property bool desktopClockShadowEnabled: Config.background.desktopClock.shadow.enabled ?? true - property real desktopClockShadowOpacity: Config.background.desktopClock.shadow.opacity ?? 0.7 - property real desktopClockShadowBlur: Config.background.desktopClock.shadow.blur ?? 0.4 - property bool desktopClockBackgroundEnabled: Config.background.desktopClock.background.enabled ?? false - property real desktopClockBackgroundOpacity: Config.background.desktopClock.background.opacity ?? 0.7 - property bool desktopClockBackgroundBlur: Config.background.desktopClock.background.blur ?? false - property bool desktopClockInvertColors: Config.background.desktopClock.invertColors ?? false - property bool backgroundEnabled: Config.background.enabled ?? true - property bool wallpaperEnabled: Config.background.wallpaperEnabled ?? true - property bool visualiserEnabled: Config.background.visualiser.enabled ?? false - property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true - property real visualiserRounding: Config.background.visualiser.rounding ?? 1 - property real visualiserSpacing: Config.background.visualiser.spacing ?? 1 - - function saveConfig() { - GlobalConfig.appearance.anim.durations.scale = root.animDurationsScale; - - GlobalConfig.appearance.font.family.material = root.fontFamilyMaterial; - GlobalConfig.appearance.font.family.mono = root.fontFamilyMono; - GlobalConfig.appearance.font.family.sans = root.fontFamilySans; - GlobalConfig.appearance.font.size.scale = root.fontSizeScale; - - GlobalConfig.appearance.padding.scale = root.paddingScale; - GlobalConfig.appearance.rounding.scale = root.roundingScale; - GlobalConfig.appearance.spacing.scale = root.spacingScale; - - GlobalConfig.appearance.transparency.enabled = root.transparencyEnabled; - GlobalConfig.appearance.transparency.base = root.transparencyBase; - GlobalConfig.appearance.transparency.layers = root.transparencyLayers; - - GlobalConfig.background.desktopClock.enabled = root.desktopClockEnabled; - GlobalConfig.background.enabled = root.backgroundEnabled; - GlobalConfig.background.desktopClock.scale = root.desktopClockScale; - GlobalConfig.background.desktopClock.position = root.desktopClockPosition; - GlobalConfig.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled; - GlobalConfig.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity; - GlobalConfig.background.desktopClock.shadow.blur = root.desktopClockShadowBlur; - GlobalConfig.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled; - GlobalConfig.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity; - GlobalConfig.background.desktopClock.background.blur = root.desktopClockBackgroundBlur; - GlobalConfig.background.desktopClock.invertColors = root.desktopClockInvertColors; - - GlobalConfig.background.wallpaperEnabled = root.wallpaperEnabled; - - GlobalConfig.background.visualiser.enabled = root.visualiserEnabled; - GlobalConfig.background.visualiser.autoHide = root.visualiserAutoHide; - GlobalConfig.background.visualiser.rounding = root.visualiserRounding; - GlobalConfig.background.visualiser.spacing = root.visualiserSpacing; - - GlobalConfig.border.rounding = root.borderRounding; - GlobalConfig.border.thickness = root.borderThickness; - } - - anchors.fill: parent - - Component { - id: appearanceRightContentComponent - - Item { - id: rightAppearanceFlickable - - ColumnLayout { - id: contentLayout - - anchors.fill: parent - spacing: 0 - - StyledText { - Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: Tokens.spacing.normal - text: qsTr("Wallpaper") - font.pointSize: Tokens.font.size.extraLarge - font.weight: 600 - } - - Loader { - id: wallpaperLoader - - Layout.fillWidth: true - Layout.fillHeight: true - Layout.bottomMargin: -Tokens.padding.large * 2 - - asynchronous: true - active: { - const isActive = root.session.activeIndex === 3; - const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; - const splitLayout = root.children[0]; - const loader = splitLayout && splitLayout.rightLoader ? splitLayout.rightLoader : null; - const shouldActivate = loader && loader.item !== null && (isActive || isAdjacent); - return shouldActivate; - } - - onStatusChanged: { - if (status === Loader.Error) { - console.error(lc, "Wallpaper loader error!"); - } - } - - sourceComponent: WallpaperGrid { - session: root.session - } - } - } - } - } - - SplitPaneLayout { - anchors.fill: parent - - leftContent: Component { - StyledFlickable { - id: sidebarFlickable - - readonly property var rootPane: root - - flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: sidebarFlickable - } - - ColumnLayout { - id: sidebarLayout - - readonly property var rootPane: sidebarFlickable.rootPane - readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded - - anchors.left: parent.left - anchors.right: parent.right - spacing: Tokens.spacing.small - - RowLayout { - spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Appearance") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - IconButton { - icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" - type: IconButton.Text - label.animate: true - onClicked: { - const shouldExpand = !sidebarLayout.allSectionsExpanded; - themeModeSection.expanded = shouldExpand; - colorVariantSection.expanded = shouldExpand; - colorSchemeSection.expanded = shouldExpand; - animationsSection.expanded = shouldExpand; - fontsSection.expanded = shouldExpand; - scalesSection.expanded = shouldExpand; - transparencySection.expanded = shouldExpand; - borderSection.expanded = shouldExpand; - backgroundSection.expanded = shouldExpand; - } - } - } - - ThemeModeSection { - id: themeModeSection - } - - ColorVariantSection { - id: colorVariantSection - } - - ColorSchemeSection { - id: colorSchemeSection - } - - AnimationsSection { - id: animationsSection - - rootPane: sidebarFlickable.rootPane - } - - FontsSection { - id: fontsSection - - rootPane: sidebarFlickable.rootPane - } - - ScalesSection { - id: scalesSection - - rootPane: sidebarFlickable.rootPane - } - - TransparencySection { - id: transparencySection - - rootPane: sidebarFlickable.rootPane - } - - BorderSection { - id: borderSection - - rootPane: sidebarFlickable.rootPane - } - - BackgroundSection { - id: backgroundSection - - rootPane: sidebarFlickable.rootPane - } - } - } - } - - rightContent: appearanceRightContentComponent - } - - LoggingCategory { - id: lc - - name: "caelestia.qml.controlcenter.appearance" - defaultLogLevel: LoggingCategory.Info - } -} diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml deleted file mode 100644 index 412441ab..00000000 --- a/modules/controlcenter/appearance/sections/AnimationsSection.qml +++ /dev/null @@ -1,44 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services - -CollapsibleSection { - id: root - - required property var rootPane - - title: qsTr("Animations") - showBackground: true - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Animation duration scale") - value: rootPane.animDurationsScale - from: 0.1 - to: 5.0 - decimals: 1 - suffix: "×" - validator: DoubleValidator { - bottom: 0.1 - top: 5.0 - } - - onValueModified: newValue => { - rootPane.animDurationsScale = newValue; - rootPane.saveConfig(); - } - } - } -} diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml deleted file mode 100644 index 3f31f10b..00000000 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ /dev/null @@ -1,351 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services - -CollapsibleSection { - id: root - - required property var rootPane - - title: qsTr("Background") - showBackground: true - - SwitchRow { - label: qsTr("Background enabled") - checked: rootPane.backgroundEnabled - onToggled: checked => { - rootPane.backgroundEnabled = checked; - rootPane.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Wallpaper enabled") - checked: rootPane.wallpaperEnabled - onToggled: checked => { - rootPane.wallpaperEnabled = checked; - rootPane.saveConfig(); - } - } - - StyledText { - Layout.topMargin: Tokens.spacing.normal - text: qsTr("Desktop Clock") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - SwitchRow { - label: qsTr("Desktop Clock enabled") - checked: rootPane.desktopClockEnabled - onToggled: checked => { - rootPane.desktopClockEnabled = checked; - rootPane.saveConfig(); - } - } - - SectionContainer { - id: posContainer - - readonly property var pos: (rootPane.desktopClockPosition || "top-left").split('-') - readonly property string currentV: pos[0] - readonly property string currentH: pos[1] - - function updateClockPos(v, h) { - rootPane.desktopClockPosition = v + "-" + h; - rootPane.saveConfig(); - } - - contentSpacing: Tokens.spacing.small - z: 1 - - StyledText { - text: qsTr("Positioning") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - SplitButtonRow { - label: qsTr("Vertical Position") - enabled: rootPane.desktopClockEnabled - - menuItems: [ - MenuItem { - property string val: "top" - - text: qsTr("Top") - icon: "vertical_align_top" - }, - MenuItem { - property string val: "middle" - - text: qsTr("Middle") - icon: "vertical_align_center" - }, - MenuItem { - property string val: "bottom" - - text: qsTr("Bottom") - icon: "vertical_align_bottom" - } - ] - - Component.onCompleted: { - for (let i = 0; i < menuItems.length; i++) { - if (menuItems[i].val === posContainer.currentV) - active = menuItems[i]; - } - } - - // The signal from SplitButtonRow - onSelected: item => posContainer.updateClockPos(item.val, posContainer.currentH) - } - - SplitButtonRow { - label: qsTr("Horizontal Position") - enabled: rootPane.desktopClockEnabled - expandedZ: 99 - - menuItems: [ - MenuItem { - property string val: "left" - - text: qsTr("Left") - icon: "align_horizontal_left" - }, - MenuItem { - property string val: "center" - - text: qsTr("Center") - icon: "align_horizontal_center" - }, - MenuItem { - property string val: "right" - - text: qsTr("Right") - icon: "align_horizontal_right" - } - ] - - Component.onCompleted: { - for (let i = 0; i < menuItems.length; i++) { - if (menuItems[i].val === posContainer.currentH) - active = menuItems[i]; - } - } - - onSelected: item => posContainer.updateClockPos(posContainer.currentV, item.val) - } - } - - SwitchRow { - label: qsTr("Invert colors") - checked: rootPane.desktopClockInvertColors - onToggled: checked => { - rootPane.desktopClockInvertColors = checked; - rootPane.saveConfig(); - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.small - - StyledText { - text: qsTr("Shadow") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - SwitchRow { - label: qsTr("Enabled") - checked: rootPane.desktopClockShadowEnabled - onToggled: checked => { - rootPane.desktopClockShadowEnabled = checked; - rootPane.saveConfig(); - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Opacity") - value: rootPane.desktopClockShadowOpacity * 100 - from: 0 - to: 100 - suffix: "%" - validator: IntValidator { - bottom: 0 - top: 100 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) - - onValueModified: newValue => { - rootPane.desktopClockShadowOpacity = newValue / 100; - rootPane.saveConfig(); - } - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Blur") - value: rootPane.desktopClockShadowBlur * 100 - from: 0 - to: 100 - suffix: "%" - validator: IntValidator { - bottom: 0 - top: 100 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) - - onValueModified: newValue => { - rootPane.desktopClockShadowBlur = newValue / 100; - rootPane.saveConfig(); - } - } - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.small - - StyledText { - text: qsTr("Background") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - SwitchRow { - label: qsTr("Enabled") - checked: rootPane.desktopClockBackgroundEnabled - onToggled: checked => { - rootPane.desktopClockBackgroundEnabled = checked; - rootPane.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Blur enabled") - checked: rootPane.desktopClockBackgroundBlur - onToggled: checked => { - rootPane.desktopClockBackgroundBlur = checked; - rootPane.saveConfig(); - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Opacity") - value: rootPane.desktopClockBackgroundOpacity * 100 - from: 0 - to: 100 - suffix: "%" - validator: IntValidator { - bottom: 0 - top: 100 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) - - onValueModified: newValue => { - rootPane.desktopClockBackgroundOpacity = newValue / 100; - rootPane.saveConfig(); - } - } - } - } - - StyledText { - Layout.topMargin: Tokens.spacing.normal - text: qsTr("Visualiser") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - SwitchRow { - label: qsTr("Visualiser enabled") - checked: rootPane.visualiserEnabled - onToggled: checked => { - rootPane.visualiserEnabled = checked; - rootPane.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Visualiser auto hide") - checked: rootPane.visualiserAutoHide - onToggled: checked => { - rootPane.visualiserAutoHide = checked; - rootPane.saveConfig(); - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Visualiser rounding") - value: rootPane.visualiserRounding - from: 0 - to: 10 - stepSize: 1 - validator: IntValidator { - bottom: 0 - top: 10 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) - - onValueModified: newValue => { - rootPane.visualiserRounding = Math.round(newValue); - rootPane.saveConfig(); - } - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Visualiser spacing") - value: rootPane.visualiserSpacing - from: 0 - to: 2 - validator: DoubleValidator { - bottom: 0 - top: 2 - } - - onValueModified: newValue => { - rootPane.visualiserSpacing = newValue; - rootPane.saveConfig(); - } - } - } -} diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml deleted file mode 100644 index 7dbd2dbe..00000000 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ /dev/null @@ -1,68 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services - -CollapsibleSection { - id: root - - required property var rootPane - - title: qsTr("Border") - showBackground: true - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Border rounding") - value: rootPane.borderRounding - from: 0.1 - to: 100 - decimals: 1 - suffix: "px" - validator: DoubleValidator { - bottom: 0.1 - top: 100 - } - - onValueModified: newValue => { - rootPane.borderRounding = newValue; - rootPane.saveConfig(); - } - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Border thickness") - value: rootPane.borderThickness - from: 0 - to: 100 - decimals: 1 - suffix: "px" - validator: DoubleValidator { - bottom: 0.1 - top: 100 - } - - onValueModified: newValue => { - rootPane.borderThickness = newValue; - rootPane.saveConfig(); - } - } - } -} diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml deleted file mode 100644 index 0a65cbdc..00000000 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ /dev/null @@ -1,147 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../../../launcher/services" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services - -CollapsibleSection { - title: qsTr("Color scheme") - description: qsTr("Available color schemes") - showBackground: true - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small / 2 - - Repeater { - model: Schemes.list - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` - readonly property bool isCurrent: schemeKey === Schemes.currentScheme - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - implicitHeight: schemeRow.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { - onClicked: { - const name = modelData.name; - const flavour = modelData.flavour; - const schemeKey = `${name} ${flavour}`; - - Schemes.currentScheme = schemeKey; - Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - - Qt.callLater(() => { - reloadTimer.restart(); - }); - } - } - - Timer { - id: reloadTimer - - interval: 300 - onTriggered: { - Schemes.reload(); - } - } - - RowLayout { - id: schemeRow - - anchors.fill: parent - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - StyledRect { - id: preview - - Layout.alignment: Qt.AlignVCenter - - border.width: 1 - border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) - - color: `#${modelData.colours?.surface}` - radius: Tokens.rounding.full - implicitWidth: iconPlaceholder.implicitWidth - implicitHeight: iconPlaceholder.implicitWidth - - MaterialIcon { - id: iconPlaceholder - - visible: false - text: "circle" - font.pointSize: Tokens.font.size.large - } - - Item { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - - implicitWidth: parent.implicitWidth / 2 - clip: true - - StyledRect { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - - implicitWidth: preview.implicitWidth - color: `#${modelData.colours?.primary}` - radius: Tokens.rounding.full - } - } - } - - Column { - Layout.fillWidth: true - spacing: 0 - - StyledText { - text: modelData.flavour ?? "" - font.pointSize: Tokens.font.size.normal - } - - StyledText { - text: modelData.name ?? "" - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - - elide: Text.ElideRight - anchors.left: parent.left - anchors.right: parent.right - } - } - - Loader { - asynchronous: true - active: isCurrent - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large - } - } - } - } - } - } -} diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml deleted file mode 100644 index c612485d..00000000 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ /dev/null @@ -1,91 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../../../launcher/services" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services - -CollapsibleSection { - title: qsTr("Color variant") - description: qsTr("Material theme variant") - showBackground: true - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small / 2 - - Repeater { - model: M3Variants.list - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal - border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 - border.color: Colours.palette.m3primary - implicitHeight: variantRow.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { - onClicked: { - const variant = modelData.variant; - - Schemes.currentVariant = variant; - Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); - - Qt.callLater(() => { - reloadTimer.restart(); - }); - } - } - - Timer { - id: reloadTimer - - interval: 300 - onTriggered: { - Schemes.reload(); - } - } - - RowLayout { - id: variantRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - MaterialIcon { - text: modelData.icon - font.pointSize: Tokens.font.size.large - fill: modelData.variant === Schemes.currentVariant ? 1 : 0 - } - - StyledText { - Layout.fillWidth: true - text: modelData.name - font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 - } - - MaterialIcon { - visible: modelData.variant === Schemes.currentVariant - text: "check" - color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.large - } - } - } - } - } -} diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml deleted file mode 100644 index 10040c58..00000000 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ /dev/null @@ -1,289 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services - -CollapsibleSection { - id: root - - required property var rootPane - - title: qsTr("Fonts") - showBackground: true - - CollapsibleSection { - id: sansFontSection - - title: qsTr("Sans-serif font family") - expanded: true - showBackground: true - nested: true - - Loader { - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: sansFontSection.expanded - - sourceComponent: StyledListView { - id: sansFontList - - property alias contentHeight: sansFontList.contentHeight - - clip: true - spacing: Tokens.spacing.small / 2 - model: Qt.fontFamilies() - - StyledScrollBar.vertical: StyledScrollBar { - flickable: sansFontList - } - - delegate: StyledRect { - required property string modelData - required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilySans - - width: ListView.view.width - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - implicitHeight: fontFamilySansRow.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { - onClicked: { - rootPane.fontFamilySans = modelData; - rootPane.saveConfig(); - } - } - - RowLayout { - id: fontFamilySansRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - StyledText { - text: modelData - font.pointSize: Tokens.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - Loader { - asynchronous: true - active: isCurrent - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large - } - } - } - } - } - } - } - - CollapsibleSection { - id: monoFontSection - - title: qsTr("Monospace font family") - expanded: false - showBackground: true - nested: true - - Loader { - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: monoFontSection.expanded - - sourceComponent: StyledListView { - id: monoFontList - - property alias contentHeight: monoFontList.contentHeight - - clip: true - spacing: Tokens.spacing.small / 2 - model: Qt.fontFamilies() - - StyledScrollBar.vertical: StyledScrollBar { - flickable: monoFontList - } - - delegate: StyledRect { - required property string modelData - required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono - - width: ListView.view.width - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - implicitHeight: fontFamilyMonoRow.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { - onClicked: { - rootPane.fontFamilyMono = modelData; - rootPane.saveConfig(); - } - } - - RowLayout { - id: fontFamilyMonoRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - StyledText { - text: modelData - font.pointSize: Tokens.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - Loader { - asynchronous: true - active: isCurrent - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large - } - } - } - } - } - } - } - - CollapsibleSection { - id: materialFontSection - - title: qsTr("Material font family") - expanded: false - showBackground: true - nested: true - - Loader { - id: materialFontLoader - - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: materialFontSection.expanded - - sourceComponent: StyledListView { - id: materialFontList - - property alias contentHeight: materialFontList.contentHeight - - clip: true - spacing: Tokens.spacing.small / 2 - model: Qt.fontFamilies().filter(f => f.startsWith("Material Symbols")) - - StyledScrollBar.vertical: StyledScrollBar { - flickable: materialFontList - } - - delegate: StyledRect { - required property string modelData - required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial - - width: ListView.view.width - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - implicitHeight: fontFamilyMaterialRow.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { - onClicked: { - rootPane.fontFamilyMaterial = modelData; - rootPane.saveConfig(); - } - } - - RowLayout { - id: fontFamilyMaterialRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - StyledText { - text: modelData - font.pointSize: Tokens.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - Loader { - asynchronous: true - active: isCurrent - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large - } - } - } - } - } - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Font size scale") - value: rootPane.fontSizeScale - from: 0.7 - to: 1.5 - decimals: 2 - suffix: "×" - validator: DoubleValidator { - bottom: 0.7 - top: 1.5 - } - - onValueModified: newValue => { - rootPane.fontSizeScale = newValue; - rootPane.saveConfig(); - } - } - } -} diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml deleted file mode 100644 index dac6226e..00000000 --- a/modules/controlcenter/appearance/sections/ScalesSection.qml +++ /dev/null @@ -1,92 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services - -CollapsibleSection { - id: root - - required property var rootPane - - title: qsTr("Scales") - showBackground: true - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Padding scale") - value: rootPane.paddingScale - from: 0.5 - to: 2.0 - decimals: 1 - suffix: "×" - validator: DoubleValidator { - bottom: 0.5 - top: 2.0 - } - - onValueModified: newValue => { - rootPane.paddingScale = newValue; - rootPane.saveConfig(); - } - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Rounding scale") - value: rootPane.roundingScale - from: 0.1 - to: 5.0 - decimals: 1 - suffix: "×" - validator: DoubleValidator { - bottom: 0.1 - top: 5.0 - } - - onValueModified: newValue => { - rootPane.roundingScale = newValue; - rootPane.saveConfig(); - } - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Spacing scale") - value: rootPane.spacingScale - from: 0.1 - to: 2.0 - decimals: 1 - suffix: "×" - validator: DoubleValidator { - bottom: 0.1 - top: 2.0 - } - - onValueModified: newValue => { - rootPane.spacingScale = newValue; - rootPane.saveConfig(); - } - } - } -} diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml deleted file mode 100644 index aab53b8b..00000000 --- a/modules/controlcenter/appearance/sections/ThemeModeSection.qml +++ /dev/null @@ -1,23 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import QtQuick -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services - -CollapsibleSection { - title: qsTr("Theme mode") - description: qsTr("Light or dark theme") - showBackground: true - - SwitchRow { - label: qsTr("Dark mode") - checked: !Colours.currentLight - onToggled: checked => { - Colours.setMode(checked ? "dark" : "light"); - } - } -} diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml deleted file mode 100644 index f2f15ba1..00000000 --- a/modules/controlcenter/appearance/sections/TransparencySection.qml +++ /dev/null @@ -1,79 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services - -CollapsibleSection { - id: root - - required property var rootPane - - title: qsTr("Transparency") - showBackground: true - - SwitchRow { - label: qsTr("Transparency enabled") - checked: rootPane.transparencyEnabled - onToggled: checked => { - rootPane.transparencyEnabled = checked; - rootPane.saveConfig(); - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Transparency base") - value: rootPane.transparencyBase * 100 - from: 0 - to: 100 - suffix: "%" - validator: IntValidator { - bottom: 0 - top: 100 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) - - onValueModified: newValue => { - rootPane.transparencyBase = newValue / 100; - rootPane.saveConfig(); - } - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Transparency layers") - value: rootPane.transparencyLayers * 100 - from: 0 - to: 100 - suffix: "%" - validator: IntValidator { - bottom: 0 - top: 100 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) - - onValueModified: newValue => { - rootPane.transparencyLayers = newValue / 100; - rootPane.saveConfig(); - } - } - } -} diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml deleted file mode 100644 index f3edb731..00000000 --- a/modules/controlcenter/audio/AudioPane.qml +++ /dev/null @@ -1,629 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services - -Item { - id: root - - required property Session session - - anchors.fill: parent - - SplitPaneLayout { - anchors.fill: parent - - leftContent: Component { - StyledFlickable { - id: leftAudioFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: leftContent.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: leftAudioFlickable - } - - ColumnLayout { - id: leftContent - - anchors.left: parent.left - anchors.right: parent.right - spacing: Tokens.spacing.normal - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Audio") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - } - - CollapsibleSection { - id: outputDevicesSection - - Layout.fillWidth: true - title: qsTr("Output devices") - expanded: true - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small - - StyledText { - text: qsTr("Devices (%1)").arg(Audio.sinks.length) - font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - } - - StyledText { - Layout.fillWidth: true - text: qsTr("All available output devices") - color: Colours.palette.m3outline - } - - Repeater { - Layout.fillWidth: true - model: Audio.sinks - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Tokens.rounding.normal - implicitHeight: outputRowLayout.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { - onClicked: { - Audio.setAudioSink(modelData); - } - } - - RowLayout { - id: outputRowLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - MaterialIcon { - text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" - font.pointSize: Tokens.font.size.large - fill: Audio.sink?.id === modelData.id ? 1 : 0 - } - - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - - text: modelData.description || qsTr("Unknown") - font.weight: Audio.sink?.id === modelData.id ? 500 : 400 - } - } - } - } - } - } - - CollapsibleSection { - id: inputDevicesSection - - Layout.fillWidth: true - title: qsTr("Input devices") - expanded: true - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small - - StyledText { - text: qsTr("Devices (%1)").arg(Audio.sources.length) - font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - } - - StyledText { - Layout.fillWidth: true - text: qsTr("All available input devices") - color: Colours.palette.m3outline - } - - Repeater { - Layout.fillWidth: true - model: Audio.sources - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Tokens.rounding.normal - implicitHeight: inputRowLayout.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { - onClicked: { - Audio.setAudioSource(modelData); - } - } - - RowLayout { - id: inputRowLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - MaterialIcon { - text: "mic" - font.pointSize: Tokens.font.size.large - fill: Audio.source?.id === modelData.id ? 1 : 0 - } - - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - - text: modelData.description || qsTr("Unknown") - font.weight: Audio.source?.id === modelData.id ? 500 : 400 - } - } - } - } - } - } - } - } - } - - rightContent: Component { - StyledFlickable { - id: rightAudioFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: rightAudioFlickable - } - - ColumnLayout { - id: contentLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "volume_up" - title: qsTr("Audio Settings") - } - - SectionHeader { - title: qsTr("Output volume") - description: qsTr("Control the volume of your output device") - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - StyledText { - text: qsTr("Volume") - font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - StyledInputField { - id: outputVolumeInput - - Layout.preferredWidth: 70 - validator: IntValidator { - bottom: 0 - top: 100 - } - enabled: !Audio.muted - - Component.onCompleted: { - text = Math.round(Audio.volume * 100).toString(); - } - - onTextEdited: text => { - if (hasFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - Audio.setVolume(val / 100); - } - } - } - - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(Audio.volume * 100).toString(); - } - } - - Connections { - function onVolumeChanged() { - if (!outputVolumeInput.hasFocus) { - outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); - } - } - - target: Audio - } - } - - StyledText { - text: "%" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - opacity: Audio.muted ? 0.5 : 1 - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: muteIcon.implicitHeight + Tokens.padding.normal * 2 - - radius: Tokens.rounding.normal - color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - - StateLayer { - onClicked: { - if (Audio.sink?.audio) { - Audio.sink.audio.muted = !Audio.sink.audio.muted; - } - } - } - - MaterialIcon { - id: muteIcon - - anchors.centerIn: parent - text: Audio.muted ? "volume_off" : "volume_up" - color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - } - } - } - - StyledSlider { - id: outputVolumeSlider - - Layout.fillWidth: true - implicitHeight: Tokens.padding.normal * 3 - - value: Audio.volume - enabled: !Audio.muted - opacity: enabled ? 1 : 0.5 - onMoved: { - Audio.setVolume(value); - if (!outputVolumeInput.hasFocus) { - outputVolumeInput.text = Math.round(value * 100).toString(); - } - } - } - } - } - - SectionHeader { - title: qsTr("Input volume") - description: qsTr("Control the volume of your input device") - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - StyledText { - text: qsTr("Volume") - font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - StyledInputField { - id: inputVolumeInput - - Layout.preferredWidth: 70 - validator: IntValidator { - bottom: 0 - top: 100 - } - enabled: !Audio.sourceMuted - - Component.onCompleted: { - text = Math.round(Audio.sourceVolume * 100).toString(); - } - - onTextEdited: text => { - if (hasFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - Audio.setSourceVolume(val / 100); - } - } - } - - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(Audio.sourceVolume * 100).toString(); - } - } - - Connections { - function onSourceVolumeChanged() { - if (!inputVolumeInput.hasFocus) { - inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); - } - } - - target: Audio - } - } - - StyledText { - text: "%" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - opacity: Audio.sourceMuted ? 0.5 : 1 - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: muteInputIcon.implicitHeight + Tokens.padding.normal * 2 - - radius: Tokens.rounding.normal - color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - - StateLayer { - onClicked: { - if (Audio.source?.audio) { - Audio.source.audio.muted = !Audio.source.audio.muted; - } - } - } - - MaterialIcon { - id: muteInputIcon - - anchors.centerIn: parent - text: "mic_off" - color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - } - } - } - - StyledSlider { - id: inputVolumeSlider - - Layout.fillWidth: true - implicitHeight: Tokens.padding.normal * 3 - - value: Audio.sourceVolume - enabled: !Audio.sourceMuted - opacity: enabled ? 1 : 0.5 - onMoved: { - Audio.setSourceVolume(value); - if (!inputVolumeInput.hasFocus) { - inputVolumeInput.text = Math.round(value * 100).toString(); - } - } - } - } - } - - SectionHeader { - title: qsTr("Applications") - description: qsTr("Control volume for individual applications") - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small - - Repeater { - model: Audio.streams - Layout.fillWidth: true - - delegate: ColumnLayout { - required property var modelData - required property int index - - Layout.fillWidth: true - spacing: Tokens.spacing.smaller - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - MaterialIcon { - text: "apps" - font.pointSize: Tokens.font.size.normal - fill: 0 - } - - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - text: Audio.getStreamName(modelData) - font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - - StyledInputField { - id: streamVolumeInput - - Layout.preferredWidth: 70 - validator: IntValidator { - bottom: 0 - top: 100 - } - enabled: !Audio.getStreamMuted(modelData) - - Component.onCompleted: { - text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); - } - - onTextEdited: text => { - if (hasFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - Audio.setStreamVolume(modelData, val / 100); - } - } - } - - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); - } - } - - Connections { - function onAudioChanged() { - if (!streamVolumeInput.hasFocus && modelData?.audio) { - streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); - } - } - - target: modelData - } - } - - StyledText { - text: "%" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - opacity: Audio.getStreamMuted(modelData) ? 0.5 : 1 - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: streamMuteIcon.implicitHeight + Tokens.padding.normal * 2 - - radius: Tokens.rounding.normal - color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - - StateLayer { - onClicked: { - Audio.setStreamMuted(modelData, !Audio.getStreamMuted(modelData)); - } - } - - MaterialIcon { - id: streamMuteIcon - - anchors.centerIn: parent - text: Audio.getStreamMuted(modelData) ? "volume_off" : "volume_up" - color: Audio.getStreamMuted(modelData) ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - } - } - } - - StyledSlider { - Layout.fillWidth: true - implicitHeight: Tokens.padding.normal * 3 - - value: Audio.getStreamVolume(modelData) - enabled: !Audio.getStreamMuted(modelData) - opacity: enabled ? 1 : 0.5 - onMoved: { - Audio.setStreamVolume(modelData, value); - if (!streamVolumeInput.hasFocus) { - streamVolumeInput.text = Math.round(value * 100).toString(); - } - } - - Connections { - function onAudioChanged() { - if (modelData?.audio) { - value = modelData.audio.volume; - } - } - - target: modelData - } - } - } - } - - StyledText { - Layout.fillWidth: true - visible: Audio.streams.length === 0 - text: qsTr("No applications currently playing audio") - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - horizontalAlignment: Text.AlignHCenter - } - } - } - } - } - } - } -} diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml deleted file mode 100644 index b2b9c0f6..00000000 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ /dev/null @@ -1,74 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import "." -import QtQuick -import Quickshell.Bluetooth -import Quickshell.Widgets -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls - -SplitPaneWithDetails { - id: root - - required property Session session - - anchors.fill: parent - - activeItem: session.bt.active - paneIdGenerator: function (item) { - return item ? (item.address || "") : ""; - } - - leftContent: Component { - StyledFlickable { - id: leftFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: deviceList.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: leftFlickable - } - - DeviceList { - id: deviceList - - anchors.left: parent.left - anchors.right: parent.right - session: root.session - } - } - } - - rightDetailsComponent: Component { - Details { - session: root.session - } - } - - rightSettingsComponent: Component { - StyledFlickable { - id: settingsFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } - - Settings { - id: settingsInner - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } - } -} diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml deleted file mode 100644 index 41575226..00000000 --- a/modules/controlcenter/bluetooth/Details.qml +++ /dev/null @@ -1,666 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell.Bluetooth -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services -import qs.utils - -StyledFlickable { - id: root - - required property Session session - readonly property BluetoothDevice device: session.bt.active - - flickableDirection: Flickable.VerticalFlick - contentHeight: detailsWrapper.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: root - } - - Item { - id: detailsWrapper - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - implicitHeight: details.implicitHeight - - DeviceDetails { - id: details - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - - session: root.session - device: root.device - - headerComponent: Component { - SettingsHeader { - icon: Icons.getBluetoothIcon(root.device?.icon ?? "") - title: root.device?.name ?? "" - } - } - - sections: [ - Component { - ColumnLayout { - spacing: Tokens.spacing.normal - - StyledText { - Layout.topMargin: Tokens.spacing.large - text: qsTr("Connection status") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Connection settings for this device") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceStatus.implicitHeight + Tokens.padding.large * 2 - - radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: deviceStatus - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - - spacing: Tokens.spacing.larger - - Toggle { - label: qsTr("Connected") - checked: root.device?.connected ?? false - toggle.onToggled: root.device.connected = checked - } - - Toggle { - label: qsTr("Paired") - checked: root.device?.paired ?? false - toggle.onToggled: { - if (root.device.paired) - root.device.forget(); - else - root.device.pair(); - } - } - - Toggle { - label: qsTr("Blocked") - checked: root.device?.blocked ?? false - toggle.onToggled: root.device.blocked = checked - } - } - } - } - }, - Component { - ColumnLayout { - spacing: Tokens.spacing.normal - - StyledText { - Layout.topMargin: Tokens.spacing.large - text: qsTr("Device properties") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Additional settings") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceProps.implicitHeight + Tokens.padding.large * 2 - - radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: deviceProps - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - - spacing: Tokens.spacing.larger - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small - - Item { - id: renameDevice - - Layout.fillWidth: true - Layout.rightMargin: Tokens.spacing.small - - implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight - - states: State { - name: "editingDeviceName" - when: root.session.bt.editingDeviceName - - AnchorChanges { - target: deviceNameEdit - anchors.top: renameDevice.top - } - PropertyChanges { - renameDevice.implicitHeight: deviceNameEdit.implicitHeight - renameLabel.opacity: 0 - deviceNameEdit.padding: root.Tokens.padding.normal - } - } - - transitions: Transition { - AnchorAnim { - type: AnchorAnim.Standard - } - Anim { - properties: "implicitHeight,opacity,padding" - } - } - - StyledText { - id: renameLabel - - anchors.left: parent.left - - text: qsTr("Device name") - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - - StyledTextField { - id: deviceNameEdit - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: renameLabel.bottom - anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Tokens.padding.normal - - text: root.device?.name ?? "" - readOnly: !root.session.bt.editingDeviceName - onAccepted: { - root.session.bt.editingDeviceName = false; - root.device.name = text; - } - - leftPadding: Tokens.padding.normal - rightPadding: Tokens.padding.normal - - background: StyledRect { - radius: Tokens.rounding.small - border.width: 2 - border.color: Colours.palette.m3primary - opacity: root.session.bt.editingDeviceName ? 1 : 0 - - Behavior on border.color { - CAnim {} - } - - Behavior on opacity { - Anim {} - } - } - - Behavior on anchors.leftMargin { - Anim {} - } - } - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 - - radius: Tokens.rounding.small - color: Colours.palette.m3secondaryContainer - opacity: root.session.bt.editingDeviceName ? 1 : 0 - scale: root.session.bt.editingDeviceName ? 1 : 0.5 - - StateLayer { - onClicked: { - root.session.bt.editingDeviceName = false; - deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); - } - - color: Colours.palette.m3onSecondaryContainer - disabled: !root.session.bt.editingDeviceName - } - - MaterialIcon { - id: cancelEditIcon - - anchors.centerIn: parent - animate: true - text: "cancel" - color: Colours.palette.m3onSecondaryContainer - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim { - type: Anim.FastSpatial - } - } - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 - - radius: root.session.bt.editingDeviceName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) - - StateLayer { - onClicked: { - root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; - if (root.session.bt.editingDeviceName) - deviceNameEdit.forceActiveFocus(); - else - deviceNameEdit.accepted(); - } - - color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - } - - MaterialIcon { - id: editIcon - - anchors.centerIn: parent - animate: true - text: root.session.bt.editingDeviceName ? "check_circle" : "edit" - color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - } - - Behavior on radius { - Anim {} - } - } - } - - Toggle { - label: qsTr("Trusted") - checked: root.device?.trusted ?? false - toggle.onToggled: root.device.trusted = checked - } - - Toggle { - label: qsTr("Wake allowed") - checked: root.device?.wakeAllowed ?? false - toggle.onToggled: root.device.wakeAllowed = checked - } - } - } - } - }, - Component { - ColumnLayout { - spacing: Tokens.spacing.normal - - StyledText { - Layout.topMargin: Tokens.spacing.large - text: qsTr("Device information") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Information about this device") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceInfo.implicitHeight + Tokens.padding.large * 2 - - radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: deviceInfo - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - - spacing: Tokens.spacing.small / 2 - - StyledText { - text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") - } - - RowLayout { - id: batteryPercent - - Layout.topMargin: Tokens.spacing.small / 2 - Layout.fillWidth: true - Layout.preferredHeight: Tokens.padding.smaller - spacing: Tokens.spacing.small / 2 - - StyledRect { - Layout.fillWidth: true - Layout.fillHeight: true - radius: Tokens.rounding.full - color: Colours.palette.m3secondaryContainer - - StyledRect { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: parent.height * 0.25 - - implicitWidth: root.device?.batteryAvailable ? batteryPercent.width * root.device.battery : 0 - radius: Tokens.rounding.full - color: Colours.palette.m3primary - } - } - } - - StyledText { - Layout.topMargin: Tokens.spacing.normal - text: qsTr("Dbus path") - } - - StyledText { - text: root.device?.dbusPath ?? "" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - - StyledText { - Layout.topMargin: Tokens.spacing.normal - text: qsTr("MAC address") - } - - StyledText { - text: root.device?.address ?? "" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - - StyledText { - Layout.topMargin: Tokens.spacing.normal - text: qsTr("Bonded") - } - - StyledText { - text: root.device?.bonded ? qsTr("Yes") : qsTr("No") - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - - StyledText { - Layout.topMargin: Tokens.spacing.normal - text: qsTr("System name") - } - - StyledText { - text: root.device?.deviceName ?? "" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - } - } - } - } - ] - } - } - - ColumnLayout { - anchors.right: fabRoot.right - anchors.bottom: fabRoot.top - anchors.bottomMargin: Tokens.padding.normal - - Repeater { - id: fabMenu - - model: ListModel { - ListElement { - name: "trust" - icon: "handshake" - } - ListElement { - name: "block" - icon: "block" - } - ListElement { - name: "pair" - icon: "missing_controller" - } - ListElement { - name: "connect" - icon: "bluetooth_connected" - } - } - - StyledClippingRect { - id: fabMenuItem - - required property var modelData - required property int index - - Layout.alignment: Qt.AlignRight - - implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.larger * 2 - - radius: Tokens.rounding.full - color: Colours.palette.m3primaryContainer - - opacity: 0 - - states: State { - name: "visible" - when: root.session.bt.fabMenuOpen - - PropertyChanges { - fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + root.Tokens.padding.large * 2 - fabMenuItem.opacity: 1 - fabMenuItemInner.opacity: 1 - } - } - - transitions: [ - Transition { - to: "visible" - - SequentialAnimation { - PauseAnimation { - duration: (fabMenu.count - 1 - fabMenuItem.index) * Tokens.anim.durations.small / 8 - } - ParallelAnimation { - Anim { - property: "implicitWidth" - type: Anim.FastSpatial - } - Anim { - property: "opacity" - type: Anim.StandardSmall - } - } - } - }, - Transition { - from: "visible" - - SequentialAnimation { - PauseAnimation { - duration: fabMenuItem.index * Tokens.anim.durations.small / 8 - } - ParallelAnimation { - Anim { - property: "implicitWidth" - type: Anim.FastSpatial - } - Anim { - property: "opacity" - type: Anim.StandardSmall - } - } - } - } - ] - - StateLayer { - onClicked: { - root.session.bt.fabMenuOpen = false; - - const name = fabMenuItem.modelData.name; - if (fabMenuItem.modelData.name !== "pair") - root.device[`${name}ed`] = !root.device[`${name}ed`]; - else if (root.device.paired) - root.device.forget(); - else - root.device.pair(); - } - } - - RowLayout { - id: fabMenuItemInner - - anchors.centerIn: parent - spacing: Tokens.spacing.normal - opacity: 0 - - MaterialIcon { - text: fabMenuItem.modelData.icon - color: Colours.palette.m3onPrimaryContainer - fill: 1 - } - - StyledText { - animate: true - text: (root.device && root.device[`${fabMenuItem.modelData.name}ed`] ? fabMenuItem.modelData.name === "connect" ? "dis" : "un" : "") + fabMenuItem.modelData.name - color: Colours.palette.m3onPrimaryContainer - font.capitalization: Font.Capitalize - Layout.preferredWidth: implicitWidth - - Behavior on Layout.preferredWidth { - Anim { - type: Anim.StandardSmall - } - } - } - } - } - } - } - - Item { - id: fabRoot - - x: root.contentX + root.width - width - y: root.contentY + root.height - height - width: 64 - height: 64 - z: 10000 - - StyledRect { - id: fabBg - - anchors.right: parent.right - anchors.top: parent.top - - implicitWidth: 64 - implicitHeight: 64 - - radius: Tokens.rounding.normal - color: root.session.bt.fabMenuOpen ? Colours.palette.m3primary : Colours.palette.m3primaryContainer - - states: State { - name: "expanded" - when: root.session.bt.fabMenuOpen - - PropertyChanges { - fabBg.implicitWidth: 48 - fabBg.implicitHeight: 48 - fabBg.radius: 48 / 2 - fab.font.pointSize: Tokens.font.size.larger - } - } - - transitions: Transition { - Anim { - properties: "implicitWidth,implicitHeight" - type: Anim.FastSpatial - } - Anim { - properties: "radius,font.pointSize" - } - } - - Elevation { - anchors.fill: parent - radius: parent.radius - z: -1 - level: fabState.containsMouse && !fabState.pressed ? 4 : 3 - } - - StateLayer { - id: fabState - - onClicked: { - root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen; - } - - color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer - } - - MaterialIcon { - id: fab - - anchors.centerIn: parent - animate: true - text: root.session.bt.fabMenuOpen ? "close" : "settings" - color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer - font.pointSize: Tokens.font.size.large - fill: 1 - } - } - } - - component Toggle: RowLayout { - required property string label - property alias checked: toggle.checked - property alias toggle: toggle - - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: parent.label - } - - StyledSwitch { - id: toggle - - cLayer: 2 - } - } -} diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml deleted file mode 100644 index 419410ee..00000000 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ /dev/null @@ -1,264 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Bluetooth -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services -import qs.utils - -DeviceList { - id: root - - required property Session session - readonly property bool smallDiscoverable: width <= 540 - readonly property bool smallPairable: width <= 480 - - title: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length) - description: qsTr("All available bluetooth devices") - activeItem: session.bt.active - - model: ScriptModel { - id: deviceModel - - values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)) - } - - headerComponent: Component { - RowLayout { - spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Bluetooth") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - ToggleButton { - toggled: Bluetooth.defaultAdapter?.enabled ?? false - icon: "power" - accent: "Tertiary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Toggle Bluetooth") - - onClicked: { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.enabled = !adapter.enabled; - } - } - - ToggleButton { - toggled: Bluetooth.defaultAdapter?.discoverable ?? false - icon: root.smallDiscoverable ? "group_search" : "" - label: root.smallDiscoverable ? "" : qsTr("Discoverable") - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Make discoverable") - - onClicked: { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.discoverable = !adapter.discoverable; - } - } - - ToggleButton { - toggled: Bluetooth.defaultAdapter?.pairable ?? false - icon: "missing_controller" - label: root.smallPairable ? "" : qsTr("Pairable") - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Make pairable") - - onClicked: { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.pairable = !adapter.pairable; - } - } - - ToggleButton { - toggled: Bluetooth.defaultAdapter?.discovering ?? false - icon: "bluetooth_searching" - accent: "Secondary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Scan for devices") - - onClicked: { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.discovering = !adapter.discovering; - } - } - - ToggleButton { - toggled: !root.session.bt.active - icon: "settings" - accent: "Primary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Bluetooth settings") - - onClicked: { - if (root.session.bt.active) - root.session.bt.active = null; - else { - root.session.bt.active = root.model.values[0] ?? null; - } - } - } - } - } - - delegate: Component { - StyledRect { - id: device - - required property BluetoothDevice modelData - readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting) - readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected - - width: ListView.view ? ListView.view.width : undefined - implicitHeight: deviceInner.implicitHeight + Tokens.padding.normal * 2 - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal - - StateLayer { - id: stateLayer - - onClicked: { - if (device.modelData) - root.session.bt.active = device.modelData; - } - } - - RowLayout { - id: deviceInner - - anchors.fill: parent - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - - radius: Tokens.rounding.normal - color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh - - StyledRect { - anchors.fill: parent - radius: parent.radius - color: Qt.alpha(device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) - } - - MaterialIcon { - id: icon - - anchors.centerIn: parent - text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "") - color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.large - fill: device.connected ? 1 : 0 - - Behavior on fill { - Anim {} - } - } - } - - ColumnLayout { - Layout.fillWidth: true - - spacing: 0 - - StyledText { - Layout.fillWidth: true - text: device.modelData ? device.modelData.name : qsTr("Unknown") - elide: Text.ElideRight - } - - StyledText { - Layout.fillWidth: true - text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "") - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - elide: Text.ElideRight - } - } - - StyledRect { - id: connectBtn - - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - - radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0) - - CircularIndicator { - anchors.fill: parent - running: device.loading - } - - StateLayer { - onClicked: { - if (device.loading) - return; - - if (device.connected) { - device.modelData.connected = false; - } else { - if (device.modelData.bonded) { - device.modelData.connected = true; - } else { - device.modelData.pair(); - } - } - } - - color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - disabled: device.loading - } - - MaterialIcon { - id: connectIcon - - anchors.centerIn: parent - animate: true - text: device.connected ? "link_off" : "link" - color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - - opacity: device.loading ? 0 : 1 - - Behavior on opacity { - Anim {} - } - } - } - } - } - } - - onItemSelected: item => session.bt.active = item -} diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml deleted file mode 100644 index 1202cc5b..00000000 --- a/modules/controlcenter/bluetooth/Settings.qml +++ /dev/null @@ -1,526 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell.Bluetooth -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services - -ColumnLayout { - id: root - - required property Session session - - spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "bluetooth" - title: qsTr("Bluetooth Settings") - } - - StyledText { - Layout.topMargin: Tokens.spacing.large - text: qsTr("Adapter status") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("General adapter settings") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: adapterStatus.implicitHeight + Tokens.padding.large * 2 - - radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: adapterStatus - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - - spacing: Tokens.spacing.larger - - Toggle { - label: qsTr("Powered") - checked: Bluetooth.defaultAdapter?.enabled ?? false - toggle.onToggled: { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.enabled = checked; - } - } - - Toggle { - label: qsTr("Discoverable") - checked: Bluetooth.defaultAdapter?.discoverable ?? false - toggle.onToggled: { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.discoverable = checked; - } - } - - Toggle { - label: qsTr("Pairable") - checked: Bluetooth.defaultAdapter?.pairable ?? false - toggle.onToggled: { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.pairable = checked; - } - } - } - } - - StyledText { - Layout.topMargin: Tokens.spacing.large - text: qsTr("Adapter properties") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Per-adapter settings") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: adapterSettings.implicitHeight + Tokens.padding.large * 2 - - radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: adapterSettings - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - - spacing: Tokens.spacing.larger - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Current adapter") - } - - Item { - id: adapterPickerButton - - property bool expanded - - implicitWidth: adapterPicker.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: adapterPicker.implicitHeight + Tokens.padding.smaller * 2 - - StateLayer { - onClicked: { - adapterPickerButton.expanded = !adapterPickerButton.expanded; - } - - radius: Tokens.rounding.small - } - - RowLayout { - id: adapterPicker - - anchors.fill: parent - anchors.margins: Tokens.padding.normal - anchors.topMargin: Tokens.padding.smaller - anchors.bottomMargin: Tokens.padding.smaller - spacing: Tokens.spacing.normal - - StyledText { - Layout.leftMargin: Tokens.padding.small - text: Bluetooth.defaultAdapter?.name ?? qsTr("None") - } - - MaterialIcon { - text: "expand_more" - } - } - - Elevation { - anchors.fill: adapterListBg - radius: adapterListBg.radius - opacity: adapterPickerButton.expanded ? 1 : 0 - scale: adapterPickerButton.expanded ? 1 : 0.7 - level: 2 - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim { - type: Anim.FastSpatial - } - } - } - - StyledClippingRect { - id: adapterListBg - - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - implicitHeight: adapterPickerButton.expanded ? adapterList.implicitHeight : adapterPickerButton.implicitHeight - - color: Colours.palette.m3secondaryContainer - radius: Tokens.rounding.small - opacity: adapterPickerButton.expanded ? 1 : 0 - scale: adapterPickerButton.expanded ? 1 : 0.7 - - ColumnLayout { - id: adapterList - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - spacing: 0 - - Repeater { - model: Bluetooth.adapters - - Item { - id: adapter - - required property BluetoothAdapter modelData - - Layout.fillWidth: true - implicitHeight: adapterInner.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { - onClicked: { - adapterPickerButton.expanded = false; - root.session.bt.currentAdapter = adapter.modelData; - } - - disabled: !adapterPickerButton.expanded - } - - RowLayout { - id: adapterInner - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - Layout.leftMargin: Tokens.padding.small - text: adapter.modelData.name - color: Colours.palette.m3onSecondaryContainer - } - - MaterialIcon { - text: "check" - color: Colours.palette.m3onSecondaryContainer - visible: adapter.modelData === root.session.bt.currentAdapter - } - } - } - } - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim { - type: Anim.FastSpatial - } - } - - Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Discoverable timeout") - } - - CustomSpinBox { - min: 0 - value: root.session.bt.currentAdapter?.discoverableTimeout ?? 0 - onValueModified: value => { - if (root.session.bt.currentAdapter) { - root.session.bt.currentAdapter.discoverableTimeout = value; - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small - - Item { - id: renameAdapter - - Layout.fillWidth: true - Layout.rightMargin: Tokens.spacing.small - - implicitHeight: renameLabel.implicitHeight + adapterNameEdit.implicitHeight - - states: State { - name: "editingAdapterName" - when: root.session.bt.editingAdapterName - - AnchorChanges { - target: adapterNameEdit - anchors.top: renameAdapter.top - } - PropertyChanges { - renameAdapter.implicitHeight: adapterNameEdit.implicitHeight - renameLabel.opacity: 0 - adapterNameEdit.padding: Tokens.padding.normal - } - } - - transitions: Transition { - AnchorAnim { - type: AnchorAnim.Standard - } - Anim { - properties: "implicitHeight,opacity,padding" - } - } - - StyledText { - id: renameLabel - - anchors.left: parent.left - - text: qsTr("Rename adapter (currently does not work)") - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - - StyledTextField { - id: adapterNameEdit - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: renameLabel.bottom - anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Tokens.padding.normal - - text: root.session.bt.currentAdapter?.name ?? "" - readOnly: !root.session.bt.editingAdapterName - onAccepted: { - root.session.bt.editingAdapterName = false; - } - - leftPadding: Tokens.padding.normal - rightPadding: Tokens.padding.normal - - background: StyledRect { - radius: Tokens.rounding.small - border.width: 2 - border.color: Colours.palette.m3primary - opacity: root.session.bt.editingAdapterName ? 1 : 0 - - Behavior on border.color { - CAnim {} - } - - Behavior on opacity { - Anim {} - } - } - - Behavior on anchors.leftMargin { - Anim {} - } - } - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 - - radius: Tokens.rounding.small - color: Colours.palette.m3secondaryContainer - opacity: root.session.bt.editingAdapterName ? 1 : 0 - scale: root.session.bt.editingAdapterName ? 1 : 0.5 - - StateLayer { - onClicked: { - root.session.bt.editingAdapterName = false; - adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? ""); - } - - color: Colours.palette.m3onSecondaryContainer - disabled: !root.session.bt.editingAdapterName - } - - MaterialIcon { - id: cancelEditIcon - - anchors.centerIn: parent - animate: true - text: "cancel" - color: Colours.palette.m3onSecondaryContainer - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim { - type: Anim.FastSpatial - } - } - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 - - radius: root.session.bt.editingAdapterName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0) - - StateLayer { - onClicked: { - root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName; - if (root.session.bt.editingAdapterName) - adapterNameEdit.forceActiveFocus(); - else - adapterNameEdit.accepted(); - } - - color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - } - - MaterialIcon { - id: editIcon - - anchors.centerIn: parent - animate: true - text: root.session.bt.editingAdapterName ? "check_circle" : "edit" - color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - } - - Behavior on radius { - Anim {} - } - } - } - } - } - - StyledText { - Layout.topMargin: Tokens.spacing.large - text: qsTr("Adapter information") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Information about the default adapter") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: adapterInfo.implicitHeight + Tokens.padding.large * 2 - - radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: adapterInfo - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - - spacing: Tokens.spacing.small / 2 - - StyledText { - text: qsTr("Adapter state") - } - - StyledText { - text: Bluetooth.defaultAdapter ? BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state) : qsTr("Unknown") - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - - StyledText { - Layout.topMargin: Tokens.spacing.normal - text: qsTr("Dbus path") - } - - StyledText { - text: Bluetooth.defaultAdapter?.dbusPath ?? "" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - - StyledText { - Layout.topMargin: Tokens.spacing.normal - text: qsTr("Adapter id") - } - - StyledText { - text: Bluetooth.defaultAdapter?.adapterId ?? "" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - } - } - - component Toggle: RowLayout { - required property string label - property alias checked: toggle.checked - property alias toggle: toggle - - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: parent.label - } - - StyledSwitch { - id: toggle - - cLayer: 2 - } - } -} diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml deleted file mode 100644 index f782838d..00000000 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ /dev/null @@ -1,114 +0,0 @@ -import ".." -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services - -StyledRect { - id: root - - property var options: [] // Array of {label: string, propertyName: string, onToggled: function, state: bool?} - property var rootItem: null // The root item that contains the properties we want to bind to - property string title: "" // Optional title text - property int rows: 1 // Number of rows - - Layout.fillWidth: true - implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - clip: true - - Behavior on implicitHeight { - Anim {} - } - - ColumnLayout { - id: layout - - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - visible: root.title !== "" - text: root.title - font.pointSize: Tokens.font.size.normal - } - - GridLayout { - id: buttonGrid - - Layout.alignment: Qt.AlignHCenter - rowSpacing: Tokens.spacing.small - columnSpacing: Tokens.spacing.small - rows: root.rows - columns: Math.ceil(root.options.length / root.rows) - - Repeater { - id: repeater - - model: root.options - - delegate: TextButton { - id: button - - required property int index - required property var modelData - - property bool _checked: false - - Layout.fillWidth: true - text: modelData.label - checked: _checked - toggle: false - type: TextButton.Tonal - - // Create binding in Component.onCompleted - Component.onCompleted: { - if (modelData.state !== undefined && modelData.state) { - _checked = modelData.state; - } else if (root.rootItem && modelData.propertyName) { - const propName = modelData.propertyName; - const rootItem = root.rootItem; - _checked = Qt.binding(function () { - return rootItem[propName] ?? false; - }); - } - } - - // Match utilities Toggles radius styling - // Each button has full rounding (not connected) since they have spacing - radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal - - // Match utilities Toggles inactive color - inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) - - // Adjust width similar to utilities toggles - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) - - onClicked: { - if (modelData.onToggled && root.rootItem && modelData.propertyName) { - const currentValue = root.rootItem[modelData.propertyName] ?? false; - modelData.onToggled(!currentValue); - } - } - - Behavior on Layout.preferredWidth { - Anim { - type: Anim.FastSpatial - } - } - - Behavior on radius { - Anim { - type: Anim.FastSpatial - } - } - } - } - } - } -} diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml deleted file mode 100644 index 075024f6..00000000 --- a/modules/controlcenter/components/DeviceDetails.qml +++ /dev/null @@ -1,74 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects - -Item { - id: root - - property Session session - property var device: null - - property Component headerComponent: null - property list sections: [] - - property Component topContent: null - property Component bottomContent: null - - implicitWidth: layout.implicitWidth - implicitHeight: layout.implicitHeight - - ColumnLayout { - id: layout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: Tokens.spacing.normal - - Loader { - id: headerLoader - - Layout.fillWidth: true - asynchronous: true - sourceComponent: root.headerComponent - visible: root.headerComponent !== null - } - - Loader { - id: topContentLoader - - Layout.fillWidth: true - asynchronous: true - sourceComponent: root.topContent - visible: root.topContent !== null - } - - Repeater { - model: root.sections - - Loader { - required property Component modelData - - Layout.fillWidth: true - asynchronous: true - sourceComponent: modelData - } - } - - Loader { - id: bottomContentLoader - - Layout.fillWidth: true - asynchronous: true - sourceComponent: root.bottomContent - visible: root.bottomContent !== null - } - } -} diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml deleted file mode 100644 index f02cc3c0..00000000 --- a/modules/controlcenter/components/DeviceList.qml +++ /dev/null @@ -1,85 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import QtQuick -import QtQuick.Layouts -import Quickshell -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services - -ColumnLayout { - id: root - - property Session session: null - property var model: null - property Component delegate: null - - property string title: "" - property string description: "" - property var activeItem: null - property Component headerComponent: null - property Component titleSuffix: null - property bool showHeader: true - property alias view: view - - signal itemSelected(var item) - - spacing: Tokens.spacing.small - - Loader { - id: headerLoader - - Layout.fillWidth: true - asynchronous: true - sourceComponent: root.headerComponent - visible: root.headerComponent !== null && root.showHeader - } - - RowLayout { - Layout.fillWidth: true - Layout.topMargin: root.headerComponent ? 0 : 0 - spacing: Tokens.spacing.small - visible: root.title !== "" || root.description !== "" - - StyledText { - visible: root.title !== "" - text: root.title - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - Loader { - asynchronous: true - sourceComponent: root.titleSuffix - visible: root.titleSuffix !== null - } - - Item { - Layout.fillWidth: true - } - } - - StyledText { - visible: root.description !== "" - Layout.fillWidth: true - text: root.description - color: Colours.palette.m3outline - } - - StyledListView { - id: view - - Layout.fillWidth: true - implicitHeight: contentHeight - - model: root.model - delegate: root.delegate - - spacing: Tokens.spacing.small / 2 - interactive: false - clip: false - } -} diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml deleted file mode 100644 index 489d3c78..00000000 --- a/modules/controlcenter/components/PaneTransition.qml +++ /dev/null @@ -1,67 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Caelestia.Config - -SequentialAnimation { - id: root - - required property Item target - property list propertyActions - - property real scaleFrom: 1.0 - property real scaleTo: 0.8 - property real opacityFrom: 1.0 - property real opacityTo: 0.0 - - ParallelAnimation { - NumberAnimation { - target: root.target - property: "opacity" - from: root.opacityFrom - to: root.opacityTo - duration: Tokens.anim.durations.normal / 2 - easing: Tokens.anim.standardAccel - } - - NumberAnimation { - target: root.target - property: "scale" - from: root.scaleFrom - to: root.scaleTo - duration: Tokens.anim.durations.normal / 2 - easing: Tokens.anim.standardAccel - } - } - - ScriptAction { - script: { - for (let i = 0; i < root.propertyActions.length; i++) { - const action = root.propertyActions[i]; - if (action.target && action.property !== undefined) { - action.target[action.property] = action.value; - } - } - } - } - - ParallelAnimation { - NumberAnimation { - target: root.target - property: "opacity" - from: root.opacityTo - to: root.opacityFrom - duration: Tokens.anim.durations.normal / 2 - easing: Tokens.anim.standardDecel - } - - NumberAnimation { - target: root.target - property: "scale" - from: root.scaleTo - to: root.scaleFrom - duration: Tokens.anim.durations.normal / 2 - easing: Tokens.anim.standardDecel - } - } -} diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml deleted file mode 100644 index 8cd493f4..00000000 --- a/modules/controlcenter/components/ReadonlySlider.qml +++ /dev/null @@ -1,67 +0,0 @@ -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.services - -ColumnLayout { - id: root - - property string label: "" - property real value: 0 - property real from: 0 - property real to: 100 - property string suffix: "" - property bool readonly: false - - spacing: Tokens.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - StyledText { - visible: root.label !== "" - text: root.label - font.pointSize: Tokens.font.size.normal - color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface - } - - Item { - Layout.fillWidth: true - } - - MaterialIcon { - visible: root.readonly - text: "lock" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - - StyledText { - text: Math.round(root.value) + (root.suffix !== "" ? " " + root.suffix : "") - font.pointSize: Tokens.font.size.normal - color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface - } - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: Tokens.padding.normal - radius: Tokens.rounding.full - color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1) - opacity: root.readonly ? 0.5 : 1.0 - - StyledRect { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - width: parent.width * ((root.value - root.from) / (root.to - root.from)) - radius: parent.radius - color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3primary - } - } -} diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml deleted file mode 100644 index c6ccbb67..00000000 --- a/modules/controlcenter/components/SettingsHeader.qml +++ /dev/null @@ -1,37 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components - -Item { - id: root - - required property string icon - required property string title - - Layout.fillWidth: true - implicitHeight: column.implicitHeight - - ColumnLayout { - id: column - - anchors.centerIn: parent - spacing: Tokens.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: root.icon - font.pointSize: Tokens.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: root.title - font.pointSize: Tokens.font.size.large - font.bold: true - } - } -} diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml deleted file mode 100644 index 73fd00ae..00000000 --- a/modules/controlcenter/components/SliderInput.qml +++ /dev/null @@ -1,180 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services - -ColumnLayout { - id: root - - property string label: "" - property real value: 0 - property real from: 0 - property real to: 100 - property real stepSize: 0 - property var validator: null - property string suffix: "" // Optional suffix text (e.g., "×", "px") - property int decimals: 1 // Number of decimal places to show (default: 1) - property var formatValueFunction: null // Optional custom format function - property var parseValueFunction: null // Optional custom parse function - property bool _initialized: false - - signal valueModified(real newValue) - - function formatValue(val: real): string { - if (formatValueFunction) { - return formatValueFunction(val); - } - // Default format function - // Check if it's an IntValidator (IntValidator doesn't have a 'decimals' property) - if (validator && validator.bottom !== undefined && validator.decimals === undefined) { - return Math.round(val).toString(); - } - // For DoubleValidator or no validator, use the decimals property - return val.toFixed(root.decimals); - } - - function parseValue(text: string): real { - if (parseValueFunction) { - return parseValueFunction(text); - } - // Default parse function - if (validator && validator.bottom !== undefined) { - // Check if it's an integer validator - if (validator.top !== undefined && validator.top === Math.floor(validator.top)) { - return parseInt(text); - } - } - return parseFloat(text); - } - - spacing: Tokens.spacing.small - - Component.onCompleted: { - // Set initialized flag after a brief delay to allow component to fully load - Qt.callLater(() => { - _initialized = true; - }); - } - - // Update input field when value changes externally (slider is already bound) - onValueChanged: { - // Only update if component is initialized to avoid issues during creation - if (root._initialized && !inputField.hasFocus) { - inputField.text = root.formatValue(root.value); - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - StyledText { - visible: root.label !== "" - text: root.label - font.pointSize: Tokens.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledInputField { - id: inputField - - Layout.preferredWidth: 70 - validator: root.validator - - Component.onCompleted: { - // Initialize text without triggering valueModified signal - text = root.formatValue(root.value); - } - - onTextEdited: text => { - if (hasFocus) { - const val = root.parseValue(text); - if (!isNaN(val)) { - // Validate against validator bounds if available - let isValid = true; - if (root.validator) { - if (root.validator.bottom !== undefined && val < root.validator.bottom) { - isValid = false; - } - if (root.validator.top !== undefined && val > root.validator.top) { - isValid = false; - } - } - - if (isValid) { - root.valueModified(val); - } - } - } - } - - onEditingFinished: { - const val = root.parseValue(text); - let isValid = true; - if (root.validator) { - if (root.validator.bottom !== undefined && val < root.validator.bottom) { - isValid = false; - } - if (root.validator.top !== undefined && val > root.validator.top) { - isValid = false; - } - } - - if (isNaN(val) || !isValid) { - text = root.formatValue(root.value); - } - } - } - - StyledText { - visible: root.suffix !== "" - text: root.suffix - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - } - } - - StyledSlider { - id: slider - - Layout.fillWidth: true - implicitHeight: Tokens.padding.normal * 3 - - from: root.from - to: root.to - stepSize: root.stepSize - - onValueChanged: { - // Update input field text in real-time as slider moves during dragging - // Always update when slider value changes (during dragging or external updates) - if (!inputField.hasFocus) { - const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; - inputField.text = root.formatValue(newValue); - } - } - - onMoved: { - const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; - root.valueModified(newValue); - if (!inputField.hasFocus) { - inputField.text = root.formatValue(newValue); - } - } - - // Use Binding to allow slider to move freely during dragging - Binding { - target: slider - property: "value" - value: root.value - when: !slider.pressed - } - } -} diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml deleted file mode 100644 index 64c7c9fc..00000000 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ /dev/null @@ -1,109 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets -import Caelestia.Config -import qs.components -import qs.components.effects - -RowLayout { - id: root - - property Component leftContent: null - property Component rightContent: null - property real leftWidthRatio: 0.4 - property int leftMinimumWidth: 420 - property var leftLoaderProperties: ({}) - property var rightLoaderProperties: ({}) - property alias leftLoader: leftLoader - property alias rightLoader: rightLoader - - spacing: 0 - - Item { - id: leftPane - - Layout.preferredWidth: Math.floor(parent.width * root.leftWidthRatio) - Layout.minimumWidth: root.leftMinimumWidth - Layout.fillHeight: true - - ClippingRectangle { - id: leftClippingRect - - anchors.fill: parent - anchors.margins: Tokens.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal / 2 - - radius: leftBorder.innerRadius - color: "transparent" - - Loader { - id: leftLoader - - anchors.fill: parent - anchors.margins: Tokens.padding.large + Tokens.padding.normal - anchors.leftMargin: Tokens.padding.large - anchors.rightMargin: Tokens.padding.large + Tokens.padding.normal / 2 - - asynchronous: true - sourceComponent: root.leftContent - - Component.onCompleted: { - for (const key in root.leftLoaderProperties) { - leftLoader[key] = root.leftLoaderProperties[key]; - } - } - } - } - - InnerBorder { - id: leftBorder - - leftThickness: 0 - rightThickness: Tokens.padding.normal / 2 - } - } - - Item { - id: rightPane - - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - id: rightClippingRect - - anchors.fill: parent - anchors.margins: Tokens.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal / 2 - - radius: rightBorder.innerRadius - color: "transparent" - - Loader { - id: rightLoader - - anchors.fill: parent - anchors.margins: Tokens.padding.large * 2 - - asynchronous: true - sourceComponent: root.rightContent - - Component.onCompleted: { - for (const key in root.rightLoaderProperties) { - rightLoader[key] = root.rightLoaderProperties[key]; - } - } - } - } - - InnerBorder { - id: rightBorder - - leftThickness: Tokens.padding.normal / 2 - } - } -} diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml deleted file mode 100644 index ad1c444d..00000000 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ /dev/null @@ -1,95 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.effects - -Item { - id: root - - required property Component leftContent - required property Component rightDetailsComponent - required property Component rightSettingsComponent - - property var activeItem: null - property var paneIdGenerator: function (item) { - return item ? String(item) : ""; - } - - property Component overlayComponent: null - - SplitPaneLayout { - id: splitLayout - - anchors.fill: parent - - leftContent: root.leftContent - - rightContent: Component { - Item { - id: rightPaneItem - - property var pane: root.activeItem - property string paneId: root.paneIdGenerator(pane) - property Component targetComponent: root.rightSettingsComponent - property Component nextComponent: root.rightSettingsComponent - - function getComponentForPane() { - return pane ? root.rightDetailsComponent : root.rightSettingsComponent; - } - - Component.onCompleted: { - targetComponent = getComponentForPane(); - nextComponent = targetComponent; - } - - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = root.paneIdGenerator(pane); - } - - Loader { - id: rightLoader - - anchors.fill: parent - - asynchronous: true - opacity: 1 - scale: 1 - transformOrigin: Item.Center - - clip: false - sourceComponent: rightPaneItem.targetComponent - } - - Behavior on paneId { - PaneTransition { - target: rightLoader - propertyActions: [ - PropertyAction { - target: rightPaneItem - property: "targetComponent" - value: rightPaneItem.nextComponent - } - ] - } - } - } - } - } - - Loader { - id: overlayLoader - - anchors.fill: parent - asynchronous: true - z: 1000 - sourceComponent: root.overlayComponent - active: root.overlayComponent !== null - } -} diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml deleted file mode 100644 index 44a615c6..00000000 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ /dev/null @@ -1,234 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import QtQuick -import Caelestia.Config -import Caelestia.Models -import qs.components -import qs.components.controls -import qs.components.effects -import qs.components.images -import qs.services - -GridView { - id: root - - required property Session session - - readonly property int minCellWidth: 200 + Tokens.spacing.normal - readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth)) - - cellWidth: width / columnsCount - cellHeight: 140 + Tokens.spacing.normal - - model: Wallpapers.list - - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: root - } - - delegate: Item { - required property var modelData - required property int index - readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent - readonly property real itemMargin: Tokens.spacing.normal / 2 - readonly property real itemRadius: Tokens.rounding.normal - - width: root.cellWidth - height: root.cellHeight - - StateLayer { - onClicked: { - Wallpapers.setWallpaper(modelData.path); - } - - anchors.fill: parent - anchors.leftMargin: itemMargin - anchors.rightMargin: itemMargin - anchors.topMargin: itemMargin - anchors.bottomMargin: itemMargin - radius: itemRadius - } - - StyledClippingRect { - id: image - - anchors.fill: parent - anchors.leftMargin: itemMargin - anchors.rightMargin: itemMargin - anchors.topMargin: itemMargin - anchors.bottomMargin: itemMargin - color: Colours.tPalette.m3surfaceContainer - radius: itemRadius - antialiasing: true - layer.enabled: true - layer.smooth: true - - CachingImage { - id: cachingImage - - path: modelData.path - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - cache: true - visible: opacity > 0 - antialiasing: true - smooth: true - sourceSize: Qt.size(width, height) - - opacity: status === Image.Ready ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutQuad - } - } - } - - // Fallback if CachingImage fails to load - Image { - id: fallbackImage - - anchors.fill: parent - source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : "" - asynchronous: true - fillMode: Image.PreserveAspectCrop - cache: true - visible: opacity > 0 - antialiasing: true - smooth: true - sourceSize: Qt.size(width, height) - - opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutQuad - } - } - } - - Timer { - id: fallbackTimer - - property bool triggered: false - - interval: 800 - running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null - onTriggered: triggered = true - } - - // Gradient overlay for filename - Rectangle { - id: filenameOverlay - - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - - implicitHeight: filenameText.implicitHeight + Tokens.padding.normal * 1.5 - radius: 0 - - gradient: Gradient { - GradientStop { - position: 0.0 - color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0) - } - GradientStop { - position: 0.3 - color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.7) - } - GradientStop { - position: 0.6 - color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.9) - } - GradientStop { - position: 1.0 - color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.95) - } - } - - opacity: 0 - - Component.onCompleted: { - opacity = 1; - } - - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic - } - } - } - } - - Rectangle { - anchors.fill: parent - anchors.leftMargin: itemMargin - anchors.rightMargin: itemMargin - anchors.topMargin: itemMargin - anchors.bottomMargin: itemMargin - color: "transparent" - radius: itemRadius + border.width - border.width: isCurrent ? 2 : 0 - border.color: Colours.palette.m3primary - antialiasing: true - smooth: true - - Behavior on border.width { - NumberAnimation { - duration: 150 - easing.type: Easing.OutQuad - } - } - - MaterialIcon { - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Tokens.padding.small - - visible: isCurrent - text: "check_circle" - color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.large - } - } - - StyledText { - id: filenameText - - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.leftMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 - anchors.rightMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 - anchors.bottomMargin: Tokens.padding.normal - - text: modelData.name - font.pointSize: Tokens.font.size.smaller - font.weight: 500 - color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface - elide: Text.ElideMiddle - maximumLineCount: 1 - horizontalAlignment: Text.AlignHCenter - - opacity: 0 - - Component.onCompleted: { - opacity = 1; - } - - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic - } - } - } - } -} diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml deleted file mode 100644 index 6bde196a..00000000 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ /dev/null @@ -1,139 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services -import qs.utils - -Item { - id: root - - required property Session session - - // General Settings - property bool enabled: Config.dashboard.enabled ?? true - property bool showOnHover: Config.dashboard.showOnHover ?? true - property int mediaUpdateInterval: GlobalConfig.dashboard.mediaUpdateInterval ?? 1000 - property int resourceUpdateInterval: GlobalConfig.dashboard.resourceUpdateInterval ?? 1000 - property int dragThreshold: Config.dashboard.dragThreshold ?? 50 - - // Dashboard Tabs - property bool showDashboard: Config.dashboard.showDashboard ?? true - property bool showMedia: Config.dashboard.showMedia ?? true - property bool showPerformance: Config.dashboard.showPerformance ?? true - property bool showWeather: Config.dashboard.showWeather ?? true - - // Performance Resources - property bool showBattery: Config.dashboard.performance.showBattery ?? false - property bool showGpu: Config.dashboard.performance.showGpu ?? true - property bool showCpu: Config.dashboard.performance.showCpu ?? true - property bool showMemory: Config.dashboard.performance.showMemory ?? true - property bool showStorage: Config.dashboard.performance.showStorage ?? true - property bool showNetwork: Config.dashboard.performance.showNetwork ?? true - - function saveConfig() { - GlobalConfig.dashboard.enabled = root.enabled; - GlobalConfig.dashboard.showOnHover = root.showOnHover; - GlobalConfig.dashboard.mediaUpdateInterval = root.mediaUpdateInterval; - GlobalConfig.dashboard.resourceUpdateInterval = root.resourceUpdateInterval; - GlobalConfig.dashboard.dragThreshold = root.dragThreshold; - GlobalConfig.dashboard.showDashboard = root.showDashboard; - GlobalConfig.dashboard.showMedia = root.showMedia; - GlobalConfig.dashboard.showPerformance = root.showPerformance; - GlobalConfig.dashboard.showWeather = root.showWeather; - GlobalConfig.dashboard.performance.showBattery = root.showBattery; - GlobalConfig.dashboard.performance.showGpu = root.showGpu; - GlobalConfig.dashboard.performance.showCpu = root.showCpu; - GlobalConfig.dashboard.performance.showMemory = root.showMemory; - GlobalConfig.dashboard.performance.showStorage = root.showStorage; - GlobalConfig.dashboard.performance.showNetwork = root.showNetwork; - // Note: sizes properties are readonly and cannot be modified - } - - anchors.fill: parent - - ClippingRectangle { - id: dashboardClippingRect - - anchors.fill: parent - anchors.margins: Tokens.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal - - radius: dashboardBorder.innerRadius - color: "transparent" - - Loader { - id: dashboardLoader - - anchors.fill: parent - anchors.margins: Tokens.padding.large + Tokens.padding.normal - anchors.leftMargin: Tokens.padding.large - anchors.rightMargin: Tokens.padding.large - - asynchronous: true - sourceComponent: dashboardContentComponent - } - } - - InnerBorder { - id: dashboardBorder - - leftThickness: 0 - rightThickness: Tokens.padding.normal - } - - Component { - id: dashboardContentComponent - - StyledFlickable { - id: dashboardFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: dashboardLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: dashboardFlickable - } - - ColumnLayout { - id: dashboardLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - - spacing: Tokens.spacing.normal - - RowLayout { - spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Dashboard") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - } - - // General Settings Section - GeneralSection { - rootItem: root - } - - // Performance Resources Section - PerformanceSection { - rootItem: root - } - } - } - } -} diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml deleted file mode 100644 index be67eab2..00000000 --- a/modules/controlcenter/dashboard/GeneralSection.qml +++ /dev/null @@ -1,128 +0,0 @@ -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.services - -SectionContainer { - id: root - - required property var rootItem - - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("General Settings") - font.pointSize: Tokens.font.size.normal - } - - SwitchRow { - label: qsTr("Enabled") - checked: root.rootItem.enabled - onToggled: checked => { - root.rootItem.enabled = checked; - root.rootItem.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show on hover") - checked: root.rootItem.showOnHover - onToggled: checked => { - root.rootItem.showOnHover = checked; - root.rootItem.saveConfig(); - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Show Dashboard tab") - checked: root.rootItem.showDashboard - onToggled: checked => { - root.rootItem.showDashboard = checked; - root.rootItem.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Show Media tab") - checked: root.rootItem.showMedia - onToggled: checked => { - root.rootItem.showMedia = checked; - root.rootItem.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Show Performance tab") - checked: root.rootItem.showPerformance - onToggled: checked => { - root.rootItem.showPerformance = checked; - root.rootItem.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Show Weather tab") - checked: root.rootItem.showWeather - onToggled: checked => { - root.rootItem.showWeather = checked; - root.rootItem.saveConfig(); - } - } - } - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Media update interval") - value: root.rootItem.mediaUpdateInterval - from: 100 - to: 10000 - stepSize: 100 - suffix: "ms" - validator: IntValidator { - bottom: 100 - top: 10000 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) - - onValueModified: newValue => { - root.rootItem.mediaUpdateInterval = Math.round(newValue); - root.rootItem.saveConfig(); - } - } - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Drag threshold") - value: root.rootItem.dragThreshold - from: 0 - to: 100 - suffix: "px" - validator: IntValidator { - bottom: 0 - top: 100 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) - - onValueModified: newValue => { - root.rootItem.dragThreshold = Math.round(newValue); - root.rootItem.saveConfig(); - } - } -} diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml deleted file mode 100644 index ea6c68b3..00000000 --- a/modules/controlcenter/dashboard/PerformanceSection.qml +++ /dev/null @@ -1,106 +0,0 @@ -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell.Services.UPower -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.services - -SectionContainer { - id: root - - required property var rootItem - // GPU toggle is hidden when gpuType is "NONE" (no GPU data available) - readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE" - // Battery toggle is hidden when no laptop battery is present - readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery - - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Performance Resources") - font.pointSize: Tokens.font.size.normal - } - - ConnectedButtonGroup { - rootItem: root.rootItem - options: { - let opts = []; - if (root.batteryAvailable) - opts.push({ - "label": qsTr("Battery"), - "propertyName": "showBattery", - "onToggled": function (checked) { - root.rootItem.showBattery = checked; - root.rootItem.saveConfig(); - } - }); - - if (root.gpuAvailable) - opts.push({ - "label": qsTr("GPU"), - "propertyName": "showGpu", - "onToggled": function (checked) { - root.rootItem.showGpu = checked; - root.rootItem.saveConfig(); - } - }); - - opts.push({ - "label": qsTr("CPU"), - "propertyName": "showCpu", - "onToggled": function (checked) { - root.rootItem.showCpu = checked; - root.rootItem.saveConfig(); - } - }, { - "label": qsTr("Memory"), - "propertyName": "showMemory", - "onToggled": function (checked) { - root.rootItem.showMemory = checked; - root.rootItem.saveConfig(); - } - }, { - "label": qsTr("Storage"), - "propertyName": "showStorage", - "onToggled": function (checked) { - root.rootItem.showStorage = checked; - root.rootItem.saveConfig(); - } - }, { - "label": qsTr("Network"), - "propertyName": "showNetwork", - "onToggled": function (checked) { - root.rootItem.showNetwork = checked; - root.rootItem.saveConfig(); - } - }); - return opts; - } - } - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Resource update interval") - value: root.rootItem.resourceUpdateInterval - from: 100 - to: 10000 - stepSize: 100 - suffix: "ms" - validator: IntValidator { - bottom: 100 - top: 10000 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) - - onValueModified: newValue => { - root.rootItem.resourceUpdateInterval = Math.round(newValue); - root.rootItem.saveConfig(); - } - } -} diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml deleted file mode 100644 index f2372dbe..00000000 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ /dev/null @@ -1,668 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import "../../launcher/services" -import "../../../utils/scripts/fuzzysort.js" as Fuzzy -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services -import qs.utils - -Item { - id: root - - required property Session session - - property var selectedApp: root.session.launcher.active - property bool hideFromLauncherChecked: false - property bool favouriteChecked: false - property string searchText: "" - property list filteredApps: [] - - function updateToggleState() { - if (!root.selectedApp) { - root.hideFromLauncherChecked = false; - root.favouriteChecked = false; - return; - } - - const appId = root.selectedApp.id || root.selectedApp.entry?.id; - - root.hideFromLauncherChecked = GlobalConfig.launcher.hiddenApps && GlobalConfig.launcher.hiddenApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.hiddenApps, appId); - root.favouriteChecked = GlobalConfig.launcher.favouriteApps && GlobalConfig.launcher.favouriteApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, appId); - } - - function saveHiddenApps(isHidden) { - if (!root.selectedApp) { - return; - } - - const appId = root.selectedApp.id || root.selectedApp.entry?.id; - - const hiddenApps = GlobalConfig.launcher.hiddenApps ? [...GlobalConfig.launcher.hiddenApps] : []; - - if (isHidden) { - if (!hiddenApps.includes(appId)) { - hiddenApps.push(appId); - } - } else { - const index = hiddenApps.indexOf(appId); - if (index !== -1) { - hiddenApps.splice(index, 1); - } - } - - GlobalConfig.launcher.hiddenApps = hiddenApps; - } - - function filterApps(search: string): list { - if (!search || search.trim() === "") { - const apps = []; - for (let i = 0; i < allAppsDb.apps.length; i++) { - apps.push(allAppsDb.apps[i]); - } - return apps; - } - - if (!allAppsDb.apps || allAppsDb.apps.length === 0) { - return []; - } - - const preparedApps = []; - for (let i = 0; i < allAppsDb.apps.length; i++) { - const app = allAppsDb.apps[i]; - const name = app.name || app.entry?.name || ""; - preparedApps.push({ - _item: app, - name: Fuzzy.prepare(name) - }); - } - - const results = Fuzzy.go(search, preparedApps, { - all: true, - keys: ["name"], - scoreFn: r => r[0].score - }); - - return results.sort((a, b) => b._score - a._score).map(r => r.obj._item); - } - - function updateFilteredApps() { - filteredApps = filterApps(searchText); - } - - anchors.fill: parent - - onSelectedAppChanged: { - root.session.launcher.active = root.selectedApp; - updateToggleState(); - } - - onSearchTextChanged: { - updateFilteredApps(); - } - - Component.onCompleted: { - updateFilteredApps(); - } - - Connections { - function onActiveChanged() { - root.selectedApp = root.session.launcher.active; - updateToggleState(); - } - - target: root.session.launcher - } - - AppDb { - id: allAppsDb - - path: `${Paths.state}/apps.sqlite` - favouriteApps: GlobalConfig.launcher.favouriteApps - entries: DesktopEntries.applications.values - } - - Connections { - function onAppsChanged() { - updateFilteredApps(); - } - - target: allAppsDb - } - - SplitPaneLayout { - anchors.fill: parent - - leftContent: Component { - ColumnLayout { - id: leftLauncherLayout - - anchors.fill: parent - - spacing: Tokens.spacing.small - - RowLayout { - spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Launcher") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - ToggleButton { - toggled: !root.session.launcher.active - icon: "settings" - accent: "Primary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Launcher settings") - - onClicked: { - if (root.session.launcher.active) { - root.session.launcher.active = null; - } else { - if (root.filteredApps.length > 0) { - root.session.launcher.active = root.filteredApps[0]; - } - } - } - } - } - - StyledText { - Layout.topMargin: Tokens.spacing.large - text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length) - font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - - StyledText { - text: qsTr("All applications available in the launcher") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal - Layout.bottomMargin: Tokens.spacing.small - - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.full - - implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight) - - MaterialIcon { - id: searchIcon - - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: Tokens.padding.normal - - text: "search" - color: Colours.palette.m3onSurfaceVariant - } - - StyledTextField { - id: searchField - - anchors.left: searchIcon.right - anchors.right: clearIcon.left - anchors.leftMargin: Tokens.spacing.small - anchors.rightMargin: Tokens.spacing.small - - topPadding: Tokens.padding.normal - bottomPadding: Tokens.padding.normal - - placeholderText: qsTr("Search applications...") - - onTextChanged: { - root.searchText = text; - } - } - - MaterialIcon { - id: clearIcon - - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: Tokens.padding.normal - - width: searchField.text ? implicitWidth : implicitWidth / 2 - opacity: { - if (!searchField.text) - return 0; - if (clearMouse.pressed) - return 0.7; - if (clearMouse.containsMouse) - return 0.8; - return 1; - } - - text: "close" - color: Colours.palette.m3onSurfaceVariant - - MouseArea { - id: clearMouse - - anchors.fill: parent - hoverEnabled: true - cursorShape: searchField.text ? Qt.PointingHandCursor : undefined - - onClicked: searchField.text = "" - } - - Behavior on width { - Anim { - type: Anim.StandardSmall - } - } - - Behavior on opacity { - Anim { - type: Anim.StandardSmall - } - } - } - } - - Loader { - id: appsListLoader - - Layout.fillWidth: true - Layout.fillHeight: true - asynchronous: true - active: true - - sourceComponent: StyledListView { - id: appsListView - - Layout.fillWidth: true - Layout.fillHeight: true - - model: root.filteredApps - spacing: Tokens.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } - - delegate: StyledRect { - required property var modelData - - readonly property bool isSelected: root.selectedApp === modelData - - width: parent ? parent.width : 0 - implicitHeight: 40 - - color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Tokens.rounding.normal - - opacity: 0 - - Component.onCompleted: { - opacity = 1; - } - - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic - } - } - - StateLayer { - onClicked: { - root.session.launcher.active = modelData; - } - } - - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - IconImage { - asynchronous: true - Layout.alignment: Qt.AlignVCenter - implicitSize: 32 - source: { - const entry = modelData.entry; - return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing"; - } - } - - StyledText { - Layout.fillWidth: true - text: modelData.name || modelData.entry?.name || qsTr("Unknown") - font.pointSize: Tokens.font.size.normal - } - - Loader { - readonly property bool isHidden: modelData ? Strings.testRegexList(GlobalConfig.launcher.hiddenApps, modelData.id) : false - readonly property bool isFav: modelData ? Strings.testRegexList(GlobalConfig.launcher.favouriteApps, modelData.id) : false - - Layout.alignment: Qt.AlignVCenter - asynchronous: true - active: isHidden || isFav - - sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null) - } - - Component { - id: hiddenIcon - - MaterialIcon { - text: "visibility_off" - fill: 1 - color: Colours.palette.m3primary - } - } - - Component { - id: favouriteIcon - - MaterialIcon { - text: "favorite" - fill: 1 - color: Colours.palette.m3primary - } - } - } - } - } - } - } - } - - rightContent: Component { - Item { - id: rightLauncherPane - - property var pane: root.session.launcher.active - property string paneId: pane ? (pane.id || pane.entry?.id || "") : "" - property Component targetComponent: settings - property Component nextComponent: settings - property var displayedApp: null - - function getComponentForPane() { - return pane ? appDetails : settings; - } - - Component.onCompleted: { - displayedApp = pane; - targetComponent = getComponentForPane(); - nextComponent = targetComponent; - } - - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = pane ? (pane.id || pane.entry?.id || "") : ""; - } - - onDisplayedAppChanged: { - if (displayedApp) { - const appId = displayedApp.id || displayedApp.entry?.id; - root.hideFromLauncherChecked = GlobalConfig.launcher.hiddenApps && GlobalConfig.launcher.hiddenApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.hiddenApps, appId); - root.favouriteChecked = GlobalConfig.launcher.favouriteApps && GlobalConfig.launcher.favouriteApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, appId); - } else { - root.hideFromLauncherChecked = false; - root.favouriteChecked = false; - } - } - - Loader { - id: rightLauncherLoader - - property var displayedApp: rightLauncherPane.displayedApp - - anchors.fill: parent - - asynchronous: true - opacity: 1 - scale: 1 - transformOrigin: Item.Center - clip: false - - sourceComponent: rightLauncherPane.targetComponent - active: true - - onItemChanged: { - if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) { - rightLauncherPane.displayedApp = rightLauncherPane.pane; - } - } - } - - Behavior on paneId { - PaneTransition { - target: rightLauncherLoader - propertyActions: [ - PropertyAction { - target: rightLauncherPane - property: "displayedApp" - value: rightLauncherPane.pane - }, - PropertyAction { - target: rightLauncherLoader - property: "active" - value: false - }, - PropertyAction { - target: rightLauncherPane - property: "targetComponent" - value: rightLauncherPane.nextComponent - }, - PropertyAction { - target: rightLauncherLoader - property: "active" - value: true - } - ] - } - } - } - } - } - - Component { - id: settings - - StyledFlickable { - id: settingsFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } - - Settings { - id: settingsInner - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } - } - - Component { - id: appDetails - - ColumnLayout { - id: appDetailsLayout - - readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null - - anchors.fill: parent - spacing: Tokens.spacing.normal - - SettingsHeader { - Layout.leftMargin: Tokens.padding.large * 2 - Layout.rightMargin: Tokens.padding.large * 2 - Layout.topMargin: Tokens.padding.large * 2 - visible: displayedApp === null - icon: "apps" - title: qsTr("Launcher Applications") - } - - Item { - Layout.alignment: Qt.AlignHCenter - Layout.leftMargin: Tokens.padding.large * 2 - Layout.rightMargin: Tokens.padding.large * 2 - Layout.topMargin: Tokens.padding.large * 2 - visible: displayedApp !== null - implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth) - implicitHeight: appIconImage.implicitHeight + Tokens.spacing.normal + appTitleText.implicitHeight - - ColumnLayout { - anchors.centerIn: parent - spacing: Tokens.spacing.normal - - IconImage { - id: appIconImage - - asynchronous: true - Layout.alignment: Qt.AlignHCenter - implicitSize: Tokens.font.size.extraLarge * 3 * 2 - source: { - const app = appDetailsLayout.displayedApp; - if (!app) - return "image-missing"; - const entry = app.entry; - if (entry && entry.icon) { - return Quickshell.iconPath(entry.icon, "image-missing"); - } - return "image-missing"; - } - } - - StyledText { - id: appTitleText - - Layout.alignment: Qt.AlignHCenter - text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" - font.pointSize: Tokens.font.size.large - font.bold: true - } - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.topMargin: Tokens.spacing.large - Layout.leftMargin: Tokens.padding.large * 2 - Layout.rightMargin: Tokens.padding.large * 2 - - StyledFlickable { - id: detailsFlickable - - anchors.fill: parent - flickableDirection: Flickable.VerticalFlick - contentHeight: debugLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } - - ColumnLayout { - id: debugLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: Tokens.spacing.normal - - SwitchRow { - Layout.topMargin: Tokens.spacing.normal - visible: appDetailsLayout.displayedApp !== null - label: qsTr("Mark as favourite") - checked: root.favouriteChecked - // disabled if: - // * app is hidden - // * app isn't in favouriteApps array but marked as favourite anyway - // ^^^ This means that this app is favourited because of a regex check - // this button can not toggle regexed apps - enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (GlobalConfig.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked) - opacity: enabled ? 1 : 0.6 - onToggled: checked => { - root.favouriteChecked = checked; - const app = appDetailsLayout.displayedApp; - if (app) { - const appId = app.id || app.entry?.id; - const favouriteApps = GlobalConfig.launcher.favouriteApps ? [...GlobalConfig.launcher.favouriteApps] : []; - if (checked) { - if (!favouriteApps.includes(appId)) { - favouriteApps.push(appId); - } - } else { - const index = favouriteApps.indexOf(appId); - if (index !== -1) { - favouriteApps.splice(index, 1); - } - } - GlobalConfig.launcher.favouriteApps = favouriteApps; - } - } - } - SwitchRow { - Layout.topMargin: Tokens.spacing.normal - visible: appDetailsLayout.displayedApp !== null - label: qsTr("Hide from launcher") - checked: root.hideFromLauncherChecked - // disabled if: - // * app is favourited - // * app isn't in hiddenApps array but marked as hidden anyway - // ^^^ This means that this app is hidden because of a regex check - // this button can not toggle regexed apps - enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (GlobalConfig.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked) - opacity: enabled ? 1 : 0.6 - onToggled: checked => { - root.hideFromLauncherChecked = checked; - const app = appDetailsLayout.displayedApp; - if (app) { - const appId = app.id || app.entry?.id; - const hiddenApps = GlobalConfig.launcher.hiddenApps ? [...GlobalConfig.launcher.hiddenApps] : []; - if (checked) { - if (!hiddenApps.includes(appId)) { - hiddenApps.push(appId); - } - } else { - const index = hiddenApps.indexOf(appId); - if (index !== -1) { - hiddenApps.splice(index, 1); - } - } - GlobalConfig.launcher.hiddenApps = hiddenApps; - } - } - } - } - } - } - } - } -} diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml deleted file mode 100644 index f0181230..00000000 --- a/modules/controlcenter/launcher/Settings.qml +++ /dev/null @@ -1,208 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services - -ColumnLayout { - id: root - - required property Session session - - spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "apps" - title: qsTr("Launcher Settings") - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("General") - description: qsTr("General launcher settings") - } - - SectionContainer { - ToggleRow { - label: qsTr("Enabled") - checked: Config.launcher.enabled - toggle.onToggled: { - GlobalConfig.launcher.enabled = checked; - } - } - - ToggleRow { - label: qsTr("Show on hover") - checked: Config.launcher.showOnHover - toggle.onToggled: { - GlobalConfig.launcher.showOnHover = checked; - } - } - - ToggleRow { - label: qsTr("Vim keybinds") - checked: GlobalConfig.launcher.vimKeybinds - toggle.onToggled: { - GlobalConfig.launcher.vimKeybinds = checked; - } - } - - ToggleRow { - label: qsTr("Enable dangerous actions") - checked: GlobalConfig.launcher.enableDangerousActions - toggle.onToggled: { - GlobalConfig.launcher.enableDangerousActions = checked; - } - } - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("Display") - description: qsTr("Display and appearance settings") - } - - SectionContainer { - contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Max shown items") - value: qsTr("%1").arg(Config.launcher.maxShown) - } - - PropertyRow { - showTopMargin: true - label: qsTr("Max wallpapers") - value: qsTr("%1").arg(Config.launcher.maxWallpapers) - } - - PropertyRow { - showTopMargin: true - label: qsTr("Drag threshold") - value: qsTr("%1 px").arg(Config.launcher.dragThreshold) - } - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("Prefixes") - description: qsTr("Command prefix settings") - } - - SectionContainer { - contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Special prefix") - value: GlobalConfig.launcher.specialPrefix || qsTr("None") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Action prefix") - value: GlobalConfig.launcher.actionPrefix || qsTr("None") - } - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("Fuzzy search") - description: qsTr("Fuzzy search settings") - } - - SectionContainer { - ToggleRow { - label: qsTr("Apps") - checked: GlobalConfig.launcher.useFuzzy.apps - toggle.onToggled: { - GlobalConfig.launcher.useFuzzy.apps = checked; - } - } - - ToggleRow { - label: qsTr("Actions") - checked: GlobalConfig.launcher.useFuzzy.actions - toggle.onToggled: { - GlobalConfig.launcher.useFuzzy.actions = checked; - } - } - - ToggleRow { - label: qsTr("Schemes") - checked: GlobalConfig.launcher.useFuzzy.schemes - toggle.onToggled: { - GlobalConfig.launcher.useFuzzy.schemes = checked; - } - } - - ToggleRow { - label: qsTr("Variants") - checked: GlobalConfig.launcher.useFuzzy.variants - toggle.onToggled: { - GlobalConfig.launcher.useFuzzy.variants = checked; - } - } - - ToggleRow { - label: qsTr("Wallpapers") - checked: GlobalConfig.launcher.useFuzzy.wallpapers - toggle.onToggled: { - GlobalConfig.launcher.useFuzzy.wallpapers = checked; - } - } - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("Sizes") - description: qsTr("Size settings for launcher items") - } - - SectionContainer { - contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Item width") - value: qsTr("%1 px").arg(Tokens.sizes.launcher.itemWidth) - } - - PropertyRow { - showTopMargin: true - label: qsTr("Item height") - value: qsTr("%1 px").arg(Tokens.sizes.launcher.itemHeight) - } - - PropertyRow { - showTopMargin: true - label: qsTr("Wallpaper width") - value: qsTr("%1 px").arg(Tokens.sizes.launcher.wallpaperWidth) - } - - PropertyRow { - showTopMargin: true - label: qsTr("Wallpaper height") - value: qsTr("%1 px").arg(Tokens.sizes.launcher.wallpaperHeight) - } - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("Hidden apps") - description: qsTr("Applications hidden from launcher") - } - - SectionContainer { - contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Total hidden") - value: qsTr("%1").arg(GlobalConfig.launcher.hiddenApps ? GlobalConfig.launcher.hiddenApps.length : 0) - } - } -} diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml deleted file mode 100644 index 1daeb4a2..00000000 --- a/modules/controlcenter/network/EthernetDetails.qml +++ /dev/null @@ -1,118 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services - -DeviceDetails { - id: root - - required property Session session - readonly property var ethernetDevice: root.session.ethernet.active - - device: ethernetDevice - - Component.onCompleted: { - if (ethernetDevice && ethernetDevice.interface) { - Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {}); - } - } - - onEthernetDeviceChanged: { - if (ethernetDevice && ethernetDevice.interface) { - Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {}); - } else { - Nmcli.ethernetDeviceDetails = null; - } - } - - headerComponent: Component { - ConnectionHeader { - icon: "cable" - title: root.ethernetDevice?.interface ?? qsTr("Unknown") - } - } - - sections: [ - Component { - ColumnLayout { - spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Connection status") - description: qsTr("Connection settings for this device") - } - - SectionContainer { - ToggleRow { - label: qsTr("Connected") - checked: root.ethernetDevice?.connected ?? false - toggle.onToggled: { - if (checked) { - Nmcli.connectEthernet(root.ethernetDevice?.connection || "", root.ethernetDevice?.interface || "", () => {}); - } else { - if (root.ethernetDevice?.connection) { - Nmcli.disconnectEthernet(root.ethernetDevice.connection, () => {}); - } - } - } - } - } - } - }, - Component { - ColumnLayout { - spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Device properties") - description: qsTr("Additional information") - } - - SectionContainer { - contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Interface") - value: root.ethernetDevice?.interface ?? qsTr("Unknown") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Connection") - value: root.ethernetDevice?.connection || qsTr("Not connected") - } - - PropertyRow { - showTopMargin: true - label: qsTr("State") - value: root.ethernetDevice?.state ?? qsTr("Unknown") - } - } - } - }, - Component { - ColumnLayout { - spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Connection information") - description: qsTr("Network connection details") - } - - SectionContainer { - ConnectionInfoSection { - deviceDetails: Nmcli.ethernetDeviceDetails - } - } - } - } - ] -} diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml deleted file mode 100644 index 3a947c86..00000000 --- a/modules/controlcenter/network/EthernetList.qml +++ /dev/null @@ -1,177 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.services - -DeviceList { - id: root - - required property Session session - - title: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) - description: qsTr("All available ethernet devices") - activeItem: session.ethernet.active - - model: Nmcli.ethernetDevices - - headerComponent: Component { - RowLayout { - spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Settings") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - ToggleButton { - toggled: !root.session.ethernet.active - icon: "settings" - accent: "Primary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - - onClicked: { - if (root.session.ethernet.active) - root.session.ethernet.active = null; - else { - root.session.ethernet.active = root.view.model.get(0)?.modelData ?? null; - } - } - } - } - } - - delegate: Component { - StyledRect { - id: ethernetItem - - required property var modelData - readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface - - width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal - - StateLayer { - id: stateLayer - - onClicked: { - root.session.ethernet.active = modelData; - } - } - - RowLayout { - id: rowLayout - - anchors.fill: parent - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - - radius: Tokens.rounding.normal - color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - - StyledRect { - anchors.fill: parent - radius: parent.radius - color: Qt.alpha(modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) - } - - MaterialIcon { - id: icon - - anchors.centerIn: parent - text: "cable" - font.pointSize: Tokens.font.size.large - fill: modelData.connected ? 1 : 0 - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - - Behavior on fill { - Anim {} - } - } - } - - ColumnLayout { - Layout.fillWidth: true - - spacing: 0 - - StyledText { - Layout.fillWidth: true - text: modelData.interface || qsTr("Unknown") - elide: Text.ElideRight - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller - - StyledText { - Layout.fillWidth: true - text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") - color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - font.weight: modelData.connected ? 500 : 400 - elide: Text.ElideRight - } - } - } - - StyledRect { - id: connectBtn - - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - - radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) - - StateLayer { - onClicked: { - if (modelData.connected && modelData.connection) { - Nmcli.disconnectEthernet(modelData.connection, () => {}); - } else { - Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); - } - } - - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - - MaterialIcon { - id: connectIcon - - anchors.centerIn: parent - animate: true - text: modelData.connected ? "link_off" : "link" - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - } - } - } - - onItemSelected: function (item) { - session.ethernet.active = item; - } -} diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml deleted file mode 100644 index d66620f1..00000000 --- a/modules/controlcenter/network/EthernetPane.qml +++ /dev/null @@ -1,50 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import Quickshell.Widgets -import Caelestia.Config -import qs.components -import qs.components.containers - -SplitPaneWithDetails { - id: root - - required property Session session - - anchors.fill: parent - - activeItem: session.ethernet.active - paneIdGenerator: function (item) { - return item ? (item.interface || "") : ""; - } - - leftContent: Component { - EthernetList { - session: root.session - } - } - - rightDetailsComponent: Component { - EthernetDetails { - session: root.session - } - } - - rightSettingsComponent: Component { - StyledFlickable { - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - clip: true - - EthernetSettings { - id: settingsInner - - anchors.left: parent.left - anchors.right: parent.right - session: root.session - } - } - } -} diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml deleted file mode 100644 index f13b6fca..00000000 --- a/modules/controlcenter/network/EthernetSettings.qml +++ /dev/null @@ -1,76 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services - -ColumnLayout { - id: root - - required property Session session - - spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "cable" - title: qsTr("Ethernet settings") - } - - StyledText { - Layout.topMargin: Tokens.spacing.large - text: qsTr("Ethernet devices") - font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Available ethernet devices") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: ethernetInfo.implicitHeight + Tokens.padding.large * 2 - - radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: ethernetInfo - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - - spacing: Tokens.spacing.small / 2 - - StyledText { - text: qsTr("Total devices") - } - - StyledText { - text: qsTr("%1").arg(Nmcli.ethernetDevices.length) - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - - StyledText { - Layout.topMargin: Tokens.spacing.normal - text: qsTr("Connected devices") - } - - StyledText { - text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - } - } -} diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml deleted file mode 100644 index d8700ecd..00000000 --- a/modules/controlcenter/network/NetworkSettings.qml +++ /dev/null @@ -1,170 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services - -ColumnLayout { - id: root - - required property Session session - - spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "router" - title: qsTr("Network Settings") - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("Ethernet") - description: qsTr("Ethernet device information") - } - - SectionContainer { - contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Total devices") - value: qsTr("%1").arg(Nmcli.ethernetDevices.length) - } - - PropertyRow { - showTopMargin: true - label: qsTr("Connected devices") - value: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) - } - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("Wireless") - description: qsTr("WiFi network settings") - } - - SectionContainer { - ToggleRow { - label: qsTr("WiFi enabled") - checked: Nmcli.wifiEnabled - toggle.onToggled: { - Nmcli.enableWifi(checked); - } - } - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("VPN") - description: qsTr("VPN provider settings") - visible: GlobalConfig.utilities.vpn.enabled || GlobalConfig.utilities.vpn.provider.length > 0 - } - - SectionContainer { - visible: GlobalConfig.utilities.vpn.enabled || GlobalConfig.utilities.vpn.provider.length > 0 - - ToggleRow { - label: qsTr("VPN enabled") - checked: GlobalConfig.utilities.vpn.enabled - toggle.onToggled: { - GlobalConfig.utilities.vpn.enabled = checked; - } - } - - PropertyRow { - showTopMargin: true - label: qsTr("Providers") - value: qsTr("%1").arg(GlobalConfig.utilities.vpn.provider.length) - } - - TextButton { - Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - text: qsTr("⚙ Manage VPN Providers") - inactiveColour: Colours.palette.m3secondaryContainer - inactiveOnColour: Colours.palette.m3onSecondaryContainer - - onClicked: { - vpnSettingsDialog.open(); - } - } - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("Current connection") - description: qsTr("Active network connection information") - } - - SectionContainer { - contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Network") - value: Nmcli.active ? Nmcli.active.ssid : (Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : qsTr("Not connected")) - } - - PropertyRow { - showTopMargin: true - visible: Nmcli.active !== null - label: qsTr("Signal strength") - value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A") - } - - PropertyRow { - showTopMargin: true - visible: Nmcli.active !== null - label: qsTr("Security") - value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") - } - - PropertyRow { - showTopMargin: true - visible: Nmcli.active !== null - label: qsTr("Frequency") - value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A") - } - } - - Popup { - id: vpnSettingsDialog - - parent: Overlay.overlay - anchors.centerIn: parent - width: Math.min(600, parent.width - Tokens.padding.large * 2) - height: Math.min(700, parent.height - Tokens.padding.large * 2) - - modal: true - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - - background: StyledRect { - color: Colours.palette.m3surface - radius: Tokens.rounding.large - } - - StyledFlickable { - anchors.fill: parent - anchors.margins: Tokens.padding.large * 1.5 - flickableDirection: Flickable.VerticalFlick - contentHeight: vpnSettingsContent.height - clip: true - - VpnSettings { - id: vpnSettingsContent - - anchors.left: parent.left - anchors.right: parent.right - session: root.session - } - } - } -} diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml deleted file mode 100644 index e02bfafb..00000000 --- a/modules/controlcenter/network/NetworkingPane.qml +++ /dev/null @@ -1,380 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import "." -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services -import qs.utils - -Item { - id: root - - required property Session session - - anchors.fill: parent - - SplitPaneLayout { - id: splitLayout - - anchors.fill: parent - - leftContent: Component { - StyledFlickable { - id: leftFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: leftContent.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: leftFlickable - } - - ColumnLayout { - id: leftContent - - anchors.left: parent.left - anchors.right: parent.right - spacing: Tokens.spacing.normal - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Network") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - ToggleButton { - toggled: Nmcli.wifiEnabled - icon: "wifi" - accent: "Tertiary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Toggle WiFi") - - onClicked: { - Nmcli.toggleWifi(null); - } - } - - ToggleButton { - toggled: Nmcli.scanning - icon: "wifi_find" - accent: "Secondary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Scan for networks") - - onClicked: { - Nmcli.rescanWifi(); - } - } - - ToggleButton { - toggled: !root.session.ethernet.active && !root.session.network.active - icon: "settings" - accent: "Primary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Network settings") - - onClicked: { - if (root.session.ethernet.active || root.session.network.active) { - root.session.ethernet.active = null; - root.session.network.active = null; - } else { - if (Nmcli.ethernetDevices.length > 0) { - root.session.ethernet.active = Nmcli.ethernetDevices[0]; - } else if (Nmcli.networks.length > 0) { - root.session.network.active = Nmcli.networks[0]; - } - } - } - } - } - - CollapsibleSection { - id: vpnListSection - - Layout.fillWidth: true - title: qsTr("VPN") - expanded: true - - Loader { - Layout.fillWidth: true - asynchronous: true - sourceComponent: Component { - VpnList { - session: root.session - showHeader: false - } - } - } - } - - CollapsibleSection { - id: ethernetListSection - - Layout.fillWidth: true - title: qsTr("Ethernet") - expanded: true - - Loader { - Layout.fillWidth: true - asynchronous: true - sourceComponent: Component { - EthernetList { - session: root.session - showHeader: false - } - } - } - } - - CollapsibleSection { - id: wirelessListSection - - Layout.fillWidth: true - title: qsTr("Wireless") - expanded: true - - Loader { - Layout.fillWidth: true - asynchronous: true - sourceComponent: Component { - WirelessList { - session: root.session - showHeader: false - } - } - } - } - } - } - } - - rightContent: Component { - Item { - id: rightPaneItem - - property var vpnPane: root.session && root.session.vpn ? root.session.vpn.active : null - property var ethernetPane: root.session && root.session.ethernet ? root.session.ethernet.active : null - property var wirelessPane: root.session && root.session.network ? root.session.network.active : null - property var pane: vpnPane || ethernetPane || wirelessPane - property string paneId: vpnPane ? ("vpn:" + (vpnPane.name || "")) : (ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings")) - property Component targetComponent: settingsComponent - property Component nextComponent: settingsComponent - - function getComponentForPane() { - if (vpnPane) - return vpnDetailsComponent; - if (ethernetPane) - return ethernetDetailsComponent; - if (wirelessPane) - return wirelessDetailsComponent; - return settingsComponent; - } - - Component.onCompleted: { - targetComponent = getComponentForPane(); - nextComponent = targetComponent; - } - - Connections { - function onActiveChanged() { - // Clear others when VPN is selected - if (root.session && root.session.vpn && root.session.vpn.active) { - if (root.session.ethernet && root.session.ethernet.active) - root.session.ethernet.active = null; - if (root.session.network && root.session.network.active) - root.session.network.active = null; - } - rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); - } - - target: root.session && root.session.vpn ? root.session.vpn : null - enabled: target !== null - } - - Connections { - function onActiveChanged() { - // Clear others when ethernet is selected - if (root.session && root.session.ethernet && root.session.ethernet.active) { - if (root.session.vpn && root.session.vpn.active) - root.session.vpn.active = null; - if (root.session.network && root.session.network.active) - root.session.network.active = null; - } - rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); - } - - target: root.session && root.session.ethernet ? root.session.ethernet : null - enabled: target !== null - } - - Connections { - function onActiveChanged() { - // Clear others when wireless is selected - if (root.session && root.session.network && root.session.network.active) { - if (root.session.vpn && root.session.vpn.active) - root.session.vpn.active = null; - if (root.session.ethernet && root.session.ethernet.active) - root.session.ethernet.active = null; - } - rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); - } - - target: root.session && root.session.network ? root.session.network : null - enabled: target !== null - } - - Loader { - id: rightLoader - - anchors.fill: parent - - opacity: 1 - scale: 1 - transformOrigin: Item.Center - clip: false - - asynchronous: true - sourceComponent: rightPaneItem.targetComponent - } - - Behavior on paneId { - PaneTransition { - target: rightLoader - propertyActions: [ - PropertyAction { - target: rightPaneItem - property: "targetComponent" - value: rightPaneItem.nextComponent - } - ] - } - } - } - } - } - - Component { - id: settingsComponent - - StyledFlickable { - id: settingsFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } - - NetworkSettings { - id: settingsInner - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } - } - - Component { - id: ethernetDetailsComponent - - StyledFlickable { - id: ethernetFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: ethernetDetailsInner.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: ethernetFlickable - } - - EthernetDetails { - id: ethernetDetailsInner - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } - } - - Component { - id: wirelessDetailsComponent - - StyledFlickable { - id: wirelessFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: wirelessDetailsInner.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: wirelessFlickable - } - - WirelessDetails { - id: wirelessDetailsInner - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } - } - - Component { - id: vpnDetailsComponent - - StyledFlickable { - id: vpnFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: vpnDetailsInner.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: vpnFlickable - } - - VpnDetails { - id: vpnDetailsInner - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } - } - - WirelessPasswordDialog { - anchors.fill: parent - session: root.session - z: 1000 - } -} diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml deleted file mode 100644 index 7ac2dc50..00000000 --- a/modules/controlcenter/network/VpnDetails.qml +++ /dev/null @@ -1,543 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services -import qs.utils - -DeviceDetails { - id: root - - required property Session session - readonly property var vpnProvider: root.session.vpn.active - readonly property bool providerEnabled: { - if (!vpnProvider || vpnProvider.index === undefined) - return false; - const provider = GlobalConfig.utilities.vpn.provider[vpnProvider.index]; - return provider && typeof provider === "object" && provider.enabled === true; - } - - device: vpnProvider - - headerComponent: Component { - ConnectionHeader { - icon: "vpn_key" - title: root.vpnProvider?.displayName ?? qsTr("Unknown") - } - } - - sections: [ - Component { - ColumnLayout { - spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Connection status") - description: qsTr("VPN connection settings") - } - - SectionContainer { - ToggleRow { - label: qsTr("Enable this provider") - checked: root.providerEnabled - toggle.onToggled: { - if (!root.vpnProvider) - return; - const providers = []; - const index = root.vpnProvider.index; - - // Copy providers and update enabled state - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - const p = GlobalConfig.utilities.vpn.provider[i]; - if (typeof p === "object") { - const newProvider = { - name: p.name, - displayName: p.displayName, - interface: p.interface - }; - - if (checked) { - // Enable this one, disable others - newProvider.enabled = (i === index); - } else { - // Just disable this one - newProvider.enabled = (i === index) ? false : (p.enabled !== false); - } - - if (p.connectCmd && p.connectCmd.length > 0) { - newProvider.connectCmd = p.connectCmd; - } - if (p.disconnectCmd && p.disconnectCmd.length > 0) { - newProvider.disconnectCmd = p.disconnectCmd; - } - - providers.push(newProvider); - } else { - providers.push(p); - } - } - - GlobalConfig.utilities.vpn.provider = providers; - } - } - - RowLayout { - Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal - spacing: Tokens.spacing.normal - - TextButton { - Layout.fillWidth: true - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - visible: root.providerEnabled - enabled: !VPN.connecting - inactiveColour: Colours.palette.m3primaryContainer - inactiveOnColour: Colours.palette.m3onPrimaryContainer - text: VPN.connected ? qsTr("Disconnect") : qsTr("Connect") - - onClicked: { - VPN.toggle(); - } - } - - TextButton { - Layout.fillWidth: true - text: qsTr("Edit Provider") - inactiveColour: Colours.palette.m3secondaryContainer - inactiveOnColour: Colours.palette.m3onSecondaryContainer - - onClicked: { - const provider = GlobalConfig.utilities.vpn.provider[root.vpnProvider.index]; - editVpnDialog.editIndex = root.vpnProvider.index; - editVpnDialog.providerName = root.vpnProvider.name; - editVpnDialog.displayName = root.vpnProvider.displayName; - editVpnDialog.interfaceName = root.vpnProvider.interface; - editVpnDialog.connectCmd = (provider && provider.connectCmd) ? provider.connectCmd.join(" ") : ""; - editVpnDialog.disconnectCmd = (provider && provider.disconnectCmd) ? provider.disconnectCmd.join(" ") : ""; - editVpnDialog.open(); - } - } - - TextButton { - Layout.fillWidth: true - text: qsTr("Delete Provider") - inactiveColour: Colours.palette.m3errorContainer - inactiveOnColour: Colours.palette.m3onErrorContainer - - onClicked: { - const providers = []; - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - if (i !== root.vpnProvider.index) { - providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - } - GlobalConfig.utilities.vpn.provider = providers; - root.session.vpn.active = null; - } - } - } - - TextButton { - Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal - visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl !== "" - text: qsTr("Open Login Page") - inactiveColour: Colours.palette.m3tertiaryContainer - inactiveOnColour: Colours.palette.m3onTertiaryContainer - - onClicked: { - Qt.openUrlExternally(VPN.status.authUrl); - } - } - - StyledText { - Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal - visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl === "" - text: qsTr("Click 'Connect' to generate authentication URL") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - } - } - } - }, - Component { - ColumnLayout { - spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Provider details") - description: qsTr("VPN provider information") - } - - SectionContainer { - contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Provider") - value: root.vpnProvider?.name ?? qsTr("Unknown") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Display name") - value: root.vpnProvider?.displayName ?? qsTr("Unknown") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Interface") - value: root.vpnProvider?.interface || qsTr("N/A") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Status") - value: { - if (!root.providerEnabled) - return qsTr("Disabled"); - if (VPN.connecting) - return qsTr("Connecting..."); - - switch (VPN.status.state) { - case "connected": - return qsTr("Connected"); - case "disconnected": - return qsTr("Disconnected"); - case "connecting": - return qsTr("Connecting..."); - case "needs-auth": - return qsTr("Authentication required"); - case "error": - return qsTr("Error"); - default: - return qsTr("Unknown"); - } - } - } - - PropertyRow { - visible: VPN.status.reason !== "" - showTopMargin: true - label: qsTr("Details") - value: VPN.status.reason - } - - PropertyRow { - showTopMargin: true - label: qsTr("Enabled") - value: root.providerEnabled ? qsTr("Yes") : qsTr("No") - } - } - } - } - ] - - // Edit VPN Dialog - Popup { - id: editVpnDialog - - property int editIndex: -1 - property string providerName: "" - property string displayName: "" - property string interfaceName: "" - property string connectCmd: "" - property string disconnectCmd: "" - - function closeWithAnimation(): void { - close(); - } - - parent: Overlay.overlay - anchors.centerIn: parent - width: Math.min(400, parent.width - Tokens.padding.large * 2) - padding: Tokens.padding.large * 1.5 - - modal: true - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - - opacity: 0 - scale: 0.7 - - enter: Transition { - Anim { - property: "opacity" - from: 0 - to: 1 - type: Anim.FastSpatial - } - Anim { - property: "scale" - from: 0.7 - to: 1 - type: Anim.FastSpatial - } - } - - exit: Transition { - Anim { - property: "opacity" - from: 1 - to: 0 - type: Anim.FastSpatial - } - Anim { - property: "scale" - from: 1 - to: 0.7 - type: Anim.FastSpatial - } - } - - Overlay.modal: Rectangle { - color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity) - } - - background: StyledRect { - color: Colours.palette.m3surfaceContainerHigh - radius: Tokens.rounding.large - - Elevation { - anchors.fill: parent - radius: parent.radius - level: 3 - z: -1 - } - } - - contentItem: ColumnLayout { - spacing: Tokens.spacing.normal - - StyledText { - text: qsTr("Edit VPN Provider") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 - - StyledText { - text: qsTr("Display Name") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: 40 - color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small - border.width: 1 - border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { - CAnim {} - } - Behavior on border.color { - CAnim {} - } - - StyledTextField { - id: displayNameField - - anchors.centerIn: parent - width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: editVpnDialog.displayName - onTextChanged: editVpnDialog.displayName = text - } - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 - - StyledText { - text: qsTr("Interface (e.g., wg0, torguard)") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: 40 - color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small - border.width: 1 - border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { - CAnim {} - } - Behavior on border.color { - CAnim {} - } - - StyledTextField { - id: interfaceNameField - - anchors.centerIn: parent - width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: editVpnDialog.interfaceName - onTextChanged: editVpnDialog.interfaceName = text - } - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 - visible: editVpnDialog.connectCmd.length > 0 - - StyledText { - text: qsTr("Connect Command") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: 40 - color: connectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small - border.width: 1 - border.color: connectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { - CAnim {} - } - Behavior on border.color { - CAnim {} - } - - StyledTextField { - id: connectCmdFieldEdit - - anchors.centerIn: parent - width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: editVpnDialog.connectCmd - onTextChanged: editVpnDialog.connectCmd = text - } - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 - visible: editVpnDialog.disconnectCmd.length > 0 - - StyledText { - text: qsTr("Disconnect Command") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: 40 - color: disconnectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small - border.width: 1 - border.color: disconnectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { - CAnim {} - } - Behavior on border.color { - CAnim {} - } - - StyledTextField { - id: disconnectCmdFieldEdit - - anchors.centerIn: parent - width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: editVpnDialog.disconnectCmd - onTextChanged: editVpnDialog.disconnectCmd = text - } - } - } - - RowLayout { - Layout.topMargin: Tokens.spacing.normal - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - TextButton { - Layout.fillWidth: true - text: qsTr("Cancel") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - onClicked: editVpnDialog.closeWithAnimation() - } - - TextButton { - Layout.fillWidth: true - text: qsTr("Save") - enabled: editVpnDialog.interfaceName.length > 0 - inactiveColour: Colours.palette.m3primaryContainer - inactiveOnColour: Colours.palette.m3onPrimaryContainer - - onClicked: { - const providers = []; - const oldProvider = GlobalConfig.utilities.vpn.provider[editVpnDialog.editIndex]; - const wasEnabled = typeof oldProvider === "object" ? (oldProvider.enabled !== false) : true; - - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - if (i === editVpnDialog.editIndex) { - const hasCommands = editVpnDialog.connectCmd.length > 0 && editVpnDialog.disconnectCmd.length > 0; - const newProvider = { - displayName: editVpnDialog.displayName || editVpnDialog.interfaceName, - enabled: wasEnabled, - interface: editVpnDialog.interfaceName, - name: editVpnDialog.providerName - }; - - if (hasCommands) { - newProvider.connectCmd = editVpnDialog.connectCmd.split(" ").filter(s => s.length > 0); - newProvider.disconnectCmd = editVpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0); - } - - providers.push(newProvider); - } else { - const p = GlobalConfig.utilities.vpn.provider[i]; - const reconstructed = { - displayName: p.displayName, - enabled: p.enabled, - interface: p.interface, - name: p.name - }; - if (p.connectCmd && p.connectCmd.length > 0) { - reconstructed.connectCmd = p.connectCmd; - } - if (p.disconnectCmd && p.disconnectCmd.length > 0) { - reconstructed.disconnectCmd = p.disconnectCmd; - } - providers.push(reconstructed); - } - } - - GlobalConfig.utilities.vpn.provider = providers; - editVpnDialog.closeWithAnimation(); - } - } - } - } - } -} diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml deleted file mode 100644 index d9266b34..00000000 --- a/modules/controlcenter/network/VpnList.qml +++ /dev/null @@ -1,830 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services - -ColumnLayout { - id: root - - required property Session session - property bool showHeader: true - property int pendingSwitchIndex: -1 - - spacing: Tokens.spacing.normal - - Connections { - function onConnectedChanged() { - if (!VPN.connected && root.pendingSwitchIndex >= 0) { - const targetIndex = root.pendingSwitchIndex; - root.pendingSwitchIndex = -1; - - const providers = []; - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - const p = GlobalConfig.utilities.vpn.provider[i]; - if (typeof p === "object") { - const newProvider = { - name: p.name, - displayName: p.displayName, - interface: p.interface, - enabled: (i === targetIndex) - }; - if (p.connectCmd && p.connectCmd.length > 0) { - newProvider.connectCmd = p.connectCmd; - } - if (p.disconnectCmd && p.disconnectCmd.length > 0) { - newProvider.disconnectCmd = p.disconnectCmd; - } - providers.push(newProvider); - } else { - providers.push(p); - } - } - GlobalConfig.utilities.vpn.provider = providers; - - Qt.callLater(function () { - VPN.toggle(); - }); - } - } - - target: VPN - } - - TextButton { - Layout.fillWidth: true - text: qsTr("+ Add VPN Provider") - inactiveColour: Colours.palette.m3primaryContainer - inactiveOnColour: Colours.palette.m3onPrimaryContainer - - onClicked: { - vpnDialog.showProviderSelection(); - } - } - - ListView { - id: listView - - Layout.fillWidth: true - Layout.preferredHeight: contentHeight - - interactive: false - spacing: Tokens.spacing.smaller - - model: ScriptModel { - values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { - const isObject = typeof provider === "object"; - const name = isObject ? (provider.name || "custom") : String(provider); - const displayName = isObject ? (provider.displayName || name) : name; - const iface = isObject ? (provider.interface || "") : ""; - const enabled = isObject ? (provider.enabled === true) : false; - - return { - index: index, - name: name, - displayName: displayName, - interface: iface, - provider: provider, - enabled: enabled - }; - }) - } - - delegate: Component { - StyledRect { - required property var modelData - required property int index - - width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal - - StateLayer { - onClicked: { - if (root.session && root.session.vpn) { - root.session.vpn.active = modelData; - } - } - } - - RowLayout { - id: rowLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - - radius: Tokens.rounding.normal - color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - - MaterialIcon { - id: icon - - anchors.centerIn: parent - text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off" - font.pointSize: Tokens.font.size.large - fill: modelData.enabled && VPN.connected ? 1 : 0 - color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - - ColumnLayout { - Layout.fillWidth: true - - spacing: 0 - - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - - text: modelData.displayName || qsTr("Unknown") - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller - - StyledText { - Layout.fillWidth: true - text: { - if (!modelData.enabled) - return qsTr("Disabled"); - - if (VPN.connecting) - return qsTr("Connecting..."); - - switch (VPN.status.state) { - case "connected": - return qsTr("Connected"); - case "disconnected": - return qsTr("Enabled"); - case "connecting": - return qsTr("Connecting..."); - case "needs-auth": - return qsTr("Auth required"); - case "error": - return qsTr("Error"); - default: - return qsTr("Enabled"); - } - } - color: { - if (!modelData.enabled) - return Colours.palette.m3outline; - if (VPN.status.state === "connected") - return Colours.palette.m3primary; - if (VPN.status.state === "error") - return Colours.palette.m3error; - if (VPN.status.state === "needs-auth") - return Colours.palette.m3tertiary; - return Colours.palette.m3onSurface; - } - font.pointSize: Tokens.font.size.small - font.weight: modelData.enabled && VPN.connected ? 500 : 400 - elide: Text.ElideRight - } - } - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - - radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) - - StateLayer { - onClicked: { - const clickedIndex = modelData.index; - - if (modelData.enabled) { - VPN.toggle(); - } else { - if (VPN.connected) { - root.pendingSwitchIndex = clickedIndex; - VPN.toggle(); - } else { - const providers = []; - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - const p = GlobalConfig.utilities.vpn.provider[i]; - if (typeof p === "object") { - const newProvider = { - name: p.name, - displayName: p.displayName, - interface: p.interface, - enabled: (i === clickedIndex) - }; - if (p.connectCmd && p.connectCmd.length > 0) { - newProvider.connectCmd = p.connectCmd; - } - if (p.disconnectCmd && p.disconnectCmd.length > 0) { - newProvider.disconnectCmd = p.disconnectCmd; - } - providers.push(newProvider); - } else { - providers.push(p); - } - } - GlobalConfig.utilities.vpn.provider = providers; - - Qt.callLater(function () { - VPN.toggle(); - }); - } - } - } - - enabled: !VPN.connecting - } - - MaterialIcon { - id: connectIcon - - anchors.centerIn: parent - text: VPN.connected && modelData.enabled ? "link_off" : "link" - color: VPN.connected && modelData.enabled ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: deleteIcon.implicitHeight + Tokens.padding.smaller * 2 - - radius: Tokens.rounding.full - color: "transparent" - - StateLayer { - onClicked: { - const providers = []; - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - if (i !== modelData.index) { - const p = GlobalConfig.utilities.vpn.provider[i]; - const reconstructed = { - name: p.name, - displayName: p.displayName, - interface: p.interface, - enabled: p.enabled - }; - if (p.connectCmd && p.connectCmd.length > 0) { - reconstructed.connectCmd = p.connectCmd; - } - if (p.disconnectCmd && p.disconnectCmd.length > 0) { - reconstructed.disconnectCmd = p.disconnectCmd; - } - providers.push(reconstructed); - } - } - GlobalConfig.utilities.vpn.provider = providers; - } - } - - MaterialIcon { - id: deleteIcon - - anchors.centerIn: parent - text: "delete" - color: Colours.palette.m3onSurface - } - } - } - } - } - } - - Popup { - id: vpnDialog - - property string currentState: "selection" - property int editIndex: -1 - property string providerName: "" - property string displayName: "" - property string interfaceName: "" - property string connectCmd: "" - property string disconnectCmd: "" - - function showProviderSelection(): void { - currentState = "selection"; - open(); - } - - function closeWithAnimation(): void { - close(); - } - - function showAddForm(providerType: string, defaultDisplayName: string): void { - editIndex = -1; - providerName = providerType; - displayName = defaultDisplayName; - interfaceName = ""; - connectCmd = ""; - disconnectCmd = ""; - - if (currentState === "selection") { - transitionToForm.start(); - } else { - currentState = "form"; - isClosing = false; - open(); - } - } - - function showEditForm(index: int): void { - const provider = GlobalConfig.utilities.vpn.provider[index]; - const isObject = typeof provider === "object"; - - editIndex = index; - providerName = isObject ? (provider.name || "custom") : String(provider); - displayName = isObject ? (provider.displayName || providerName) : providerName; - interfaceName = isObject ? (provider.interface || "") : ""; - connectCmd = isObject && provider.connectCmd ? provider.connectCmd.join(" ") : ""; - disconnectCmd = isObject && provider.disconnectCmd ? provider.disconnectCmd.join(" ") : ""; - - currentState = "form"; - open(); - } - - parent: Overlay.overlay - x: Math.round((parent.width - width) / 2) - y: Math.round((parent.height - height) / 2) - implicitWidth: Math.min(400, parent.width - Tokens.padding.large * 2) - padding: Tokens.padding.large * 1.5 - - modal: true - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - - opacity: 0 - scale: 0.7 - - enter: Transition { - ParallelAnimation { - Anim { - property: "opacity" - from: 0 - to: 1 - type: Anim.Emphasized - } - Anim { - property: "scale" - from: 0.7 - to: 1 - type: Anim.Emphasized - } - } - } - - exit: Transition { - ParallelAnimation { - Anim { - property: "opacity" - from: 1 - to: 0 - type: Anim.EmphasizedSmall - } - Anim { - property: "scale" - from: 1 - to: 0.7 - type: Anim.EmphasizedSmall - } - } - } - - Overlay.modal: Rectangle { - color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) - } - - onClosed: { - currentState = "selection"; - } - - background: StyledRect { - color: Colours.palette.m3surfaceContainerHigh - radius: Tokens.rounding.large - - Elevation { - anchors.fill: parent - radius: parent.radius - level: 3 - z: -1 - } - - Behavior on implicitHeight { - Anim { - type: Anim.Emphasized - } - } - } - - contentItem: Item { - implicitHeight: vpnDialog.currentState === "selection" ? selectionContent.implicitHeight : formContent.implicitHeight - - Behavior on implicitHeight { - Anim { - type: Anim.Emphasized - } - } - - ColumnLayout { - id: selectionContent - - anchors.fill: parent - spacing: Tokens.spacing.normal - visible: vpnDialog.currentState === "selection" - opacity: vpnDialog.currentState === "selection" ? 1 : 0 - - Behavior on opacity { - Anim { - type: Anim.EmphasizedSmall - } - } - - StyledText { - text: qsTr("Add VPN Provider") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - StyledText { - Layout.fillWidth: true - text: qsTr("Choose a provider to add") - wrapMode: Text.WordWrap - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - - TextButton { - Layout.topMargin: Tokens.spacing.normal - Layout.fillWidth: true - text: qsTr("NetBird") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - onClicked: { - const providers = []; - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - providers.push({ - name: "netbird", - displayName: "NetBird", - interface: "wt0" - }); - GlobalConfig.utilities.vpn.provider = providers; - vpnDialog.closeWithAnimation(); - } - } - - TextButton { - Layout.fillWidth: true - text: qsTr("Tailscale") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - onClicked: { - const providers = []; - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - providers.push({ - name: "tailscale", - displayName: "Tailscale", - interface: "tailscale0" - }); - GlobalConfig.utilities.vpn.provider = providers; - vpnDialog.closeWithAnimation(); - } - } - - TextButton { - Layout.fillWidth: true - text: qsTr("Cloudflare WARP") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - onClicked: { - const providers = []; - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - providers.push({ - name: "warp", - displayName: "Cloudflare WARP", - interface: "CloudflareWARP" - }); - GlobalConfig.utilities.vpn.provider = providers; - vpnDialog.closeWithAnimation(); - } - } - - TextButton { - Layout.fillWidth: true - text: qsTr("WireGuard") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - onClicked: { - vpnDialog.showAddForm("wireguard", "WireGuard"); - } - } - - TextButton { - Layout.fillWidth: true - text: qsTr("Custom") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - onClicked: { - vpnDialog.showAddForm("custom", "Custom VPN"); - } - } - - TextButton { - Layout.topMargin: Tokens.spacing.normal - Layout.fillWidth: true - text: qsTr("Cancel") - inactiveColour: Colours.palette.m3secondaryContainer - inactiveOnColour: Colours.palette.m3onSecondaryContainer - onClicked: vpnDialog.closeWithAnimation() - } - } - - ColumnLayout { - id: formContent - - anchors.fill: parent - spacing: Tokens.spacing.normal - visible: vpnDialog.currentState === "form" - opacity: vpnDialog.currentState === "form" ? 1 : 0 - - Behavior on opacity { - Anim { - type: Anim.EmphasizedSmall - } - } - - StyledText { - text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName) - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 - - StyledText { - text: qsTr("Display Name") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: 40 - color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small - border.width: 1 - border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { - CAnim {} - } - Behavior on border.color { - CAnim {} - } - - StyledTextField { - id: displayNameField - - anchors.centerIn: parent - width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: vpnDialog.displayName - onTextChanged: vpnDialog.displayName = text - } - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 - - StyledText { - text: qsTr("Interface (e.g., wg0, torguard)") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: 40 - color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small - border.width: 1 - border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { - CAnim {} - } - Behavior on border.color { - CAnim {} - } - - StyledTextField { - id: interfaceNameField - - anchors.centerIn: parent - width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: vpnDialog.interfaceName - onTextChanged: vpnDialog.interfaceName = text - } - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 - visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") - - StyledText { - text: qsTr("Connect Command (e.g., wg-quick up wg0)") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: 40 - color: connectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small - border.width: 1 - border.color: connectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { - CAnim {} - } - Behavior on border.color { - CAnim {} - } - - StyledTextField { - id: connectCmdField - - anchors.centerIn: parent - width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: vpnDialog.connectCmd - onTextChanged: vpnDialog.connectCmd = text - } - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 - visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") - - StyledText { - text: qsTr("Disconnect Command (e.g., wg-quick down wg0)") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: 40 - color: disconnectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small - border.width: 1 - border.color: disconnectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { - CAnim {} - } - Behavior on border.color { - CAnim {} - } - - StyledTextField { - id: disconnectCmdField - - anchors.centerIn: parent - width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: vpnDialog.disconnectCmd - onTextChanged: vpnDialog.disconnectCmd = text - } - } - } - - RowLayout { - Layout.topMargin: Tokens.spacing.normal - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - TextButton { - Layout.fillWidth: true - text: qsTr("Cancel") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - onClicked: vpnDialog.closeWithAnimation() - } - - TextButton { - Layout.fillWidth: true - text: qsTr("Save") - enabled: { - const hasCommands = vpnDialog.connectCmd.length > 0 || vpnDialog.disconnectCmd.length > 0; - if (hasCommands) { - return vpnDialog.interfaceName.length > 0 && vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0; - } - return vpnDialog.interfaceName.length > 0; - } - inactiveColour: Colours.palette.m3primaryContainer - inactiveOnColour: Colours.palette.m3onPrimaryContainer - - onClicked: { - const providers = []; - const hasCommands = vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0; - const newProvider = { - displayName: vpnDialog.displayName || vpnDialog.interfaceName, - enabled: false, - interface: vpnDialog.interfaceName, - name: vpnDialog.providerName - }; - - if (hasCommands) { - newProvider.connectCmd = vpnDialog.connectCmd.split(" ").filter(s => s.length > 0); - newProvider.disconnectCmd = vpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0); - } - - if (vpnDialog.editIndex >= 0) { - const oldProvider = GlobalConfig.utilities.vpn.provider[vpnDialog.editIndex]; - if (typeof oldProvider === "object" && oldProvider.enabled !== undefined) { - newProvider.enabled = oldProvider.enabled; - } - - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - if (i === vpnDialog.editIndex) { - providers.push(newProvider); - } else { - providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - } - } else { - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - providers.push(newProvider); - } - - GlobalConfig.utilities.vpn.provider = providers; - vpnDialog.closeWithAnimation(); - } - } - } - } - } - - SequentialAnimation { - id: transitionToForm - - ParallelAnimation { - Anim { - target: selectionContent - property: "opacity" - to: 0 - type: Anim.EmphasizedSmall - } - } - - ScriptAction { - script: { - vpnDialog.currentState = "form"; - } - } - - ParallelAnimation { - Anim { - target: formContent - property: "opacity" - to: 1 - type: Anim.EmphasizedSmall - } - } - } - } -} diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml deleted file mode 100644 index 064f32a1..00000000 --- a/modules/controlcenter/network/VpnSettings.qml +++ /dev/null @@ -1,257 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services - -ColumnLayout { - id: root - - required property Session session - - spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "vpn_key" - title: qsTr("VPN Settings") - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("General") - description: qsTr("VPN configuration") - } - - SectionContainer { - ToggleRow { - label: qsTr("VPN enabled") - checked: GlobalConfig.utilities.vpn.enabled - toggle.onToggled: { - GlobalConfig.utilities.vpn.enabled = checked; - } - } - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("Providers") - description: qsTr("Manage VPN providers") - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - ListView { - Layout.fillWidth: true - Layout.preferredHeight: contentHeight - - interactive: false - spacing: Tokens.spacing.smaller - - model: ScriptModel { - values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { - const isObject = typeof provider === "object"; - const name = isObject ? (provider.name || "custom") : String(provider); - const displayName = isObject ? (provider.displayName || name) : name; - const iface = isObject ? (provider.interface || "") : ""; - - return { - index: index, - name: name, - displayName: displayName, - interface: iface, - provider: provider, - isActive: index === 0 - }; - }) - } - - delegate: Component { - StyledRect { - required property var modelData - required property int index - - width: ListView.view ? ListView.view.width : undefined - implicitHeight: 60 - color: Colours.tPalette.m3surfaceContainerHigh - radius: Tokens.rounding.normal - - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - spacing: Tokens.spacing.normal - - MaterialIcon { - text: modelData.isActive ? "vpn_key" : "vpn_key_off" - font.pointSize: Tokens.font.size.large - color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - - StyledText { - text: modelData.displayName - font.weight: modelData.isActive ? 500 : 400 - } - - StyledText { - text: qsTr("%1 • %2").arg(modelData.name).arg(modelData.interface || qsTr("No interface")) - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - } - } - - IconButton { - icon: modelData.isActive ? "arrow_downward" : "arrow_upward" - visible: !modelData.isActive || GlobalConfig.utilities.vpn.provider.length > 1 - onClicked: { - const providers = []; - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - const p = GlobalConfig.utilities.vpn.provider[i]; - const reconstructed = { - name: p.name, - displayName: p.displayName, - interface: p.interface, - enabled: p.enabled - }; - if (p.connectCmd && p.connectCmd.length > 0) { - reconstructed.connectCmd = p.connectCmd; - } - if (p.disconnectCmd && p.disconnectCmd.length > 0) { - reconstructed.disconnectCmd = p.disconnectCmd; - } - providers.push(reconstructed); - } - - if (modelData.isActive && index < providers.length - 1) { - // Move down - const temp = providers[index]; - providers[index] = providers[index + 1]; - providers[index + 1] = temp; - } else if (!modelData.isActive) { - // Make active (move to top) - const provider = providers.splice(index, 1)[0]; - providers.unshift(provider); - } - - GlobalConfig.utilities.vpn.provider = providers; - } - } - - IconButton { - icon: "delete" - onClicked: { - const providers = []; - for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - if (i !== index) { - const p = GlobalConfig.utilities.vpn.provider[i]; - const reconstructed = { - name: p.name, - displayName: p.displayName, - interface: p.interface, - enabled: p.enabled - }; - if (p.connectCmd && p.connectCmd.length > 0) { - reconstructed.connectCmd = p.connectCmd; - } - if (p.disconnectCmd && p.disconnectCmd.length > 0) { - reconstructed.disconnectCmd = p.disconnectCmd; - } - providers.push(reconstructed); - } - } - GlobalConfig.utilities.vpn.provider = providers; - } - } - } - } - } - } - - TextButton { - Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal - text: qsTr("+ Add Provider") - inactiveColour: Colours.palette.m3primaryContainer - inactiveOnColour: Colours.palette.m3onPrimaryContainer - - onClicked: { - addProviderDialog.open(); - } - } - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("Quick Add") - description: qsTr("Add common VPN providers") - } - - SectionContainer { - contentSpacing: Tokens.spacing.smaller - - TextButton { - Layout.fillWidth: true - text: qsTr("+ Add NetBird") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - - onClicked: { - const providers = [...GlobalConfig.utilities.vpn.provider]; - providers.push({ - name: "netbird", - displayName: "NetBird", - interface: "wt0" - }); - GlobalConfig.utilities.vpn.provider = providers; - } - } - - TextButton { - Layout.fillWidth: true - text: qsTr("+ Add Tailscale") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - - onClicked: { - const providers = [...GlobalConfig.utilities.vpn.provider]; - providers.push({ - name: "tailscale", - displayName: "Tailscale", - interface: "tailscale0" - }); - GlobalConfig.utilities.vpn.provider = providers; - } - } - - TextButton { - Layout.fillWidth: true - text: qsTr("+ Add Cloudflare WARP") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - - onClicked: { - const providers = [...GlobalConfig.utilities.vpn.provider]; - providers.push({ - name: "warp", - displayName: "Cloudflare WARP", - interface: "CloudflareWARP" - }); - GlobalConfig.utilities.vpn.provider = providers; - } - } - } -} diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml deleted file mode 100644 index e39744ca..00000000 --- a/modules/controlcenter/network/WirelessDetails.qml +++ /dev/null @@ -1,213 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import "." -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services -import qs.utils - -DeviceDetails { - id: root - - required property Session session - readonly property var network: root.session.network.active - - function checkSavedProfile(): void { - if (network && network.ssid) { - Nmcli.loadSavedConnections(() => {}); - } - } - - function updateDeviceDetails(): void { - if (network && network.ssid) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); - if (isActive) { - Nmcli.getWirelessDeviceDetails(""); - } else { - Nmcli.wirelessDeviceDetails = null; - } - } else { - Nmcli.wirelessDeviceDetails = null; - } - } - - device: network - - Component.onCompleted: { - updateDeviceDetails(); - checkSavedProfile(); - } - - onNetworkChanged: { - connectionUpdateTimer.stop(); - if (network && network.ssid) { - connectionUpdateTimer.start(); - } - updateDeviceDetails(); - checkSavedProfile(); - } - - headerComponent: Component { - ConnectionHeader { - icon: root.network?.isSecure ? "lock" : "wifi" - title: root.network?.ssid ?? qsTr("Unknown") - } - } - - sections: [ - Component { - ColumnLayout { - spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Connection status") - description: qsTr("Connection settings for this network") - } - - SectionContainer { - ToggleRow { - label: qsTr("Connected") - checked: root.network?.active ?? false - toggle.onToggled: { - if (checked) { - NetworkConnection.handleConnect(root.network, root.session, null); - } else { - Nmcli.disconnectFromNetwork(); - } - } - } - - TextButton { - Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - visible: { - if (!root.network || !root.network.ssid) { - return false; - } - return Nmcli.hasSavedProfile(root.network.ssid); - } - inactiveColour: Colours.palette.m3secondaryContainer - inactiveOnColour: Colours.palette.m3onSecondaryContainer - text: qsTr("Forget Network") - - onClicked: { - if (root.network && root.network.ssid) { - if (root.network.active) { - Nmcli.disconnectFromNetwork(); - } - Nmcli.forgetNetwork(root.network.ssid); - } - } - } - } - } - }, - Component { - ColumnLayout { - spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Network properties") - description: qsTr("Additional information") - } - - SectionContainer { - contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("SSID") - value: root.network?.ssid ?? qsTr("Unknown") - } - - PropertyRow { - showTopMargin: true - label: qsTr("BSSID") - value: root.network?.bssid ?? qsTr("Unknown") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Signal strength") - value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Frequency") - value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Security") - value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A") - } - } - } - }, - Component { - ColumnLayout { - spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Connection information") - description: qsTr("Network connection details") - } - - SectionContainer { - ConnectionInfoSection { - deviceDetails: Nmcli.wirelessDeviceDetails - } - } - } - } - ] - - Connections { - function onActiveChanged() { - updateDeviceDetails(); - } - function onWirelessDeviceDetailsChanged() { - if (network && network.ssid) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); - if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { - connectionUpdateTimer.stop(); - } - } - } - - target: Nmcli - } - - Timer { - id: connectionUpdateTimer - - interval: 500 - repeat: true - running: network && network.ssid - onTriggered: { - if (network) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); - if (isActive) { - if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { - Nmcli.getWirelessDeviceDetails("", () => {}); - } else { - connectionUpdateTimer.stop(); - } - } else { - if (Nmcli.wirelessDeviceDetails !== null) { - Nmcli.wirelessDeviceDetails = null; - } - } - } - } - } -} diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml deleted file mode 100644 index b6acd079..00000000 --- a/modules/controlcenter/network/WirelessList.qml +++ /dev/null @@ -1,227 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import "." -import QtQuick -import QtQuick.Layouts -import Quickshell -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services -import qs.utils - -DeviceList { - id: root - - required property Session session - - function checkSavedProfileForNetwork(ssid: string): void { - if (ssid && ssid.length > 0) { - Nmcli.loadSavedConnections(() => {}); - } - } - - title: qsTr("Networks (%1)").arg(Nmcli.networks.length) - description: qsTr("All available WiFi networks") - activeItem: session.network.active - - titleSuffix: Component { - StyledText { - visible: Nmcli.scanning - text: qsTr("Scanning...") - color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.small - } - } - - model: ScriptModel { - values: [...Nmcli.networks].sort((a, b) => { - if (a.active !== b.active) - return b.active - a.active; - return b.strength - a.strength; - }) - } - - headerComponent: Component { - RowLayout { - spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Settings") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - ToggleButton { - toggled: Nmcli.wifiEnabled - icon: "wifi" - accent: "Tertiary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - - onClicked: { - Nmcli.toggleWifi(null); - } - } - - ToggleButton { - toggled: Nmcli.scanning - icon: "wifi_find" - accent: "Secondary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - - onClicked: { - Nmcli.rescanWifi(); - } - } - - ToggleButton { - toggled: !root.session.network.active - icon: "settings" - accent: "Primary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller - - onClicked: { - if (root.session.network.active) - root.session.network.active = null; - else { - root.session.network.active = root.view.model.get(0)?.modelData ?? null; - } - } - } - } - } - - delegate: Component { - StyledRect { - required property var modelData - - width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal - - StateLayer { - onClicked: { - root.session.network.active = modelData; - if (modelData && modelData.ssid) { - root.checkSavedProfileForNetwork(modelData.ssid); - } - } - } - - RowLayout { - id: rowLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - - spacing: Tokens.spacing.normal - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - - radius: Tokens.rounding.normal - color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - - MaterialIcon { - id: icon - - anchors.centerIn: parent - text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure) - font.pointSize: Tokens.font.size.large - fill: modelData.active ? 1 : 0 - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - - ColumnLayout { - Layout.fillWidth: true - - spacing: 0 - - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - - text: modelData.ssid || qsTr("Unknown") - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.smaller - - StyledText { - Layout.fillWidth: true - text: { - if (modelData.active) - return qsTr("Connected"); - if (modelData.isSecure && modelData.security && modelData.security.length > 0) { - return modelData.security; - } - if (modelData.isSecure) - return qsTr("Secured"); - return qsTr("Open"); - } - color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - font.weight: modelData.active ? 500 : 400 - elide: Text.ElideRight - } - } - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - - radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) - - StateLayer { - onClicked: { - if (modelData.active) { - Nmcli.disconnectFromNetwork(); - } else { - NetworkConnection.handleConnect(modelData, root.session, null); - } - } - } - - MaterialIcon { - id: connectIcon - - anchors.centerIn: parent - text: modelData.active ? "link_off" : "link" - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - } - } - } - - onItemSelected: function (item) { - session.network.active = item; - if (item && item.ssid) { - checkSavedProfileForNetwork(item.ssid); - } - } -} diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml deleted file mode 100644 index 43689176..00000000 --- a/modules/controlcenter/network/WirelessPane.qml +++ /dev/null @@ -1,57 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import Quickshell.Widgets -import Caelestia.Config -import qs.components -import qs.components.containers - -SplitPaneWithDetails { - id: root - - required property Session session - - anchors.fill: parent - - activeItem: session.network.active - paneIdGenerator: function (item) { - return item ? (item.ssid || item.bssid || "") : ""; - } - - leftContent: Component { - WirelessList { - session: root.session - } - } - - rightDetailsComponent: Component { - WirelessDetails { - session: root.session - } - } - - rightSettingsComponent: Component { - StyledFlickable { - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - clip: true - - WirelessSettings { - id: settingsInner - - anchors.left: parent.left - anchors.right: parent.right - session: root.session - } - } - } - - overlayComponent: Component { - WirelessPasswordDialog { - anchors.fill: parent - session: root.session - } - } -} diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml deleted file mode 100644 index ac88f9fc..00000000 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ /dev/null @@ -1,525 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "." -import QtQuick -import QtQuick.Layouts -import Quickshell -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services -import qs.utils - -Item { - id: root - - required property Session session - - readonly property var network: { - if (session.network.pendingNetwork) { - return session.network.pendingNetwork; - } - if (session.network.active) { - return session.network.active; - } - return null; - } - - property bool isClosing: false - - function checkConnectionStatus(): void { - if (!root.visible || !connectButton.connecting) { - return; - } - - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - - if (isConnected) { - connectionSuccessTimer.start(); - return; - } - - if (Nmcli.pendingConnection === null && connectButton.connecting) { - if (connectionMonitor.repeatCount > 10) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - } - } - - function closeDialog(): void { - if (isClosing) { - return; - } - - isClosing = true; - passwordContainer.passwordBuffer = ""; - connectButton.connecting = false; - connectButton.hasError = false; - connectButton.text = qsTr("Connect"); - connectionMonitor.stop(); - } - - visible: session.network.showPasswordDialog || isClosing - enabled: session.network.showPasswordDialog && !isClosing - focus: enabled - - Keys.onEscapePressed: { - closeDialog(); - } - - Rectangle { - anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.5) - opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 - - Behavior on opacity { - Anim {} - } - - MouseArea { - anchors.fill: parent - onClicked: closeDialog() - } - } - - StyledRect { - id: dialog - - anchors.centerIn: parent - - implicitWidth: 400 - implicitHeight: content.implicitHeight + Tokens.padding.large * 2 - - radius: Tokens.rounding.normal - color: Colours.tPalette.m3surface - opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 - scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 - Keys.onEscapePressed: closeDialog() - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - - ParallelAnimation { - running: root.isClosing - onFinished: { - if (root.isClosing) { - root.session.network.showPasswordDialog = false; - root.isClosing = false; - } - } - - Anim { - target: dialog - property: "opacity" - to: 0 - } - Anim { - target: dialog - property: "scale" - to: 0.7 - } - } - - ColumnLayout { - id: content - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - - spacing: Tokens.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "lock" - font.pointSize: Tokens.font.size.extraLarge * 2 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Enter password") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - } - - StyledText { - id: statusText - - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Tokens.spacing.small - visible: connectButton.connecting || connectButton.hasError - text: { - if (connectButton.hasError) { - return qsTr("Connection failed. Please check your password and try again."); - } - if (connectButton.connecting) { - return qsTr("Connecting..."); - } - return ""; - } - color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - font.weight: 400 - wrapMode: Text.WordWrap - Layout.maximumWidth: parent.width - Tokens.padding.large * 2 - } - - Item { - id: passwordContainer - - property string passwordBuffer: "" - - Layout.topMargin: Tokens.spacing.large - Layout.fillWidth: true - implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) - focus: true - Keys.onPressed: event => { - if (!activeFocus) { - forceActiveFocus(); - } - - if (event.key === Qt.Key_Escape) { - event.accepted = false; - closeDialog(); - } - - if (connectButton.hasError && event.text && event.text.length > 0) { - connectButton.hasError = false; - } - - if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { - if (connectButton.enabled) { - connectButton.clicked(); - } - event.accepted = true; - } else if (event.key === Qt.Key_Backspace) { - if (event.modifiers & Qt.ControlModifier) { - passwordBuffer = ""; - } else { - passwordBuffer = passwordBuffer.slice(0, -1); - } - event.accepted = true; - } else if (event.text && event.text.length > 0) { - if (event.key === Qt.Key_Tab) { - event.accepted = false; - return; - } - passwordBuffer += event.text; - event.accepted = true; - } - } - - Connections { - function onShowPasswordDialogChanged(): void { - if (root.session.network.showPasswordDialog) { - Qt.callLater(() => { - passwordContainer.forceActiveFocus(); - passwordContainer.passwordBuffer = ""; - connectButton.hasError = false; - }); - } - } - - target: root.session.network - } - - Connections { - function onVisibleChanged(): void { - if (root.visible) { - Qt.callLater(() => { - passwordContainer.forceActiveFocus(); - }); - } - } - - target: root - } - - StyledRect { - anchors.fill: parent - radius: Tokens.rounding.normal - color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer - border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0) - border.color: { - if (connectButton.hasError) { - return Colours.palette.m3error; - } - if (passwordContainer.activeFocus) { - return Colours.palette.m3primary; - } - return root.visible ? Colours.palette.m3outline : "transparent"; - } - - Behavior on border.color { - CAnim {} - } - - Behavior on border.width { - CAnim {} - } - - Behavior on color { - CAnim {} - } - } - - StateLayer { - onClicked: { - passwordContainer.forceActiveFocus(); - } - - hoverEnabled: false - cursorShape: Qt.IBeamCursor - } - - StyledText { - id: placeholder - - anchors.centerIn: parent - text: qsTr("Password") - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - font.family: Tokens.font.family.mono - opacity: passwordContainer.passwordBuffer ? 0 : 1 - - Behavior on opacity { - Anim {} - } - } - - ListView { - id: charList - - readonly property int fullWidth: count * (implicitHeight + spacing) - spacing - - anchors.centerIn: parent - implicitWidth: fullWidth - implicitHeight: Tokens.font.size.normal - - orientation: Qt.Horizontal - spacing: Tokens.spacing.small / 2 - interactive: false - - model: ScriptModel { - values: passwordContainer.passwordBuffer.split("") - } - - delegate: StyledRect { - id: ch - - implicitWidth: implicitHeight - implicitHeight: charList.implicitHeight - - color: Colours.palette.m3onSurface - radius: Tokens.rounding.small / 2 - - opacity: 0 - scale: 0 - Component.onCompleted: { - opacity = 1; - scale = 1; - } - ListView.onRemove: removeAnim.start() - - SequentialAnimation { - id: removeAnim - - PropertyAction { - target: ch - property: "ListView.delayRemove" - value: true - } - ParallelAnimation { - Anim { - target: ch - property: "opacity" - to: 0 - } - Anim { - target: ch - property: "scale" - to: 0.5 - } - } - PropertyAction { - target: ch - property: "ListView.delayRemove" - value: false - } - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim { - type: Anim.FastSpatial - } - } - } - - Behavior on implicitWidth { - Anim {} - } - } - } - - RowLayout { - Layout.topMargin: Tokens.spacing.normal - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - TextButton { - id: cancelButton - - Layout.fillWidth: true - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - inactiveColour: Colours.palette.m3secondaryContainer - inactiveOnColour: Colours.palette.m3onSecondaryContainer - text: qsTr("Cancel") - - onClicked: root.closeDialog() - } - - TextButton { - id: connectButton - - property bool connecting: false - property bool hasError: false - - Layout.fillWidth: true - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - inactiveColour: Colours.palette.m3primary - inactiveOnColour: Colours.palette.m3onPrimary - text: qsTr("Connect") - enabled: passwordContainer.passwordBuffer.length > 0 && !connecting - - onClicked: { - if (!root.network || connecting) { - return; - } - - const password = passwordContainer.passwordBuffer; - if (!password || password.length === 0) { - return; - } - - hasError = false; - connecting = true; - enabled = false; - text = qsTr("Connecting..."); - - NetworkConnection.connectWithPassword(root.network, password, result => { - if (result && result.success) {} else if (result && result.needsPassword) { - connectionMonitor.stop(); - connecting = false; - hasError = true; - enabled = true; - text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } else { - connectionMonitor.stop(); - connecting = false; - hasError = true; - enabled = true; - text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - }); - - connectionMonitor.start(); - } - } - } - } - } - - Timer { - id: connectionMonitor - - property int repeatCount: 0 - - interval: 1000 - repeat: true - triggeredOnStart: false - onTriggered: { - repeatCount++; - checkConnectionStatus(); - } - - onRunningChanged: { - if (!running) { - repeatCount = 0; - } - } - } - - Timer { - id: connectionSuccessTimer - - interval: 500 - onTriggered: { - if (root.visible && Nmcli.active && Nmcli.active.ssid) { - const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - if (stillConnected) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - closeDialog(); - } - } - } - } - - Connections { - function onActiveChanged() { - if (root.visible) { - checkConnectionStatus(); - } - } - function onConnectionFailed(ssid: string) { - if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - Nmcli.forgetNetwork(ssid); - } - } - - target: Nmcli - } -} diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml deleted file mode 100644 index f3f1fd4b..00000000 --- a/modules/controlcenter/network/WirelessSettings.qml +++ /dev/null @@ -1,73 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services - -ColumnLayout { - id: root - - required property Session session - - spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "wifi" - title: qsTr("Network settings") - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("WiFi status") - description: qsTr("General WiFi settings") - } - - SectionContainer { - ToggleRow { - label: qsTr("WiFi enabled") - checked: Nmcli.wifiEnabled - toggle.onToggled: { - Nmcli.enableWifi(checked); - } - } - } - - SectionHeader { - Layout.topMargin: Tokens.spacing.large - title: qsTr("Network information") - description: qsTr("Current network connection") - } - - SectionContainer { - contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Connected network") - value: Nmcli.active ? Nmcli.active.ssid : qsTr("Not connected") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Signal strength") - value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Security") - value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Frequency") - value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A") - } - } -} diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml deleted file mode 100644 index eda4e213..00000000 --- a/modules/controlcenter/notifications/NotificationsPane.qml +++ /dev/null @@ -1,425 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services - -Item { - id: root - - required property Session session - - property bool notificationsExpire: GlobalConfig.notifs.expire ?? true - property string notificationsFullscreen: GlobalConfig.notifs.fullscreen ?? "on" - property bool notificationsOpenExpanded: Config.notifs.openExpanded ?? false - property int notificationsDefaultExpireTimeout: GlobalConfig.notifs.defaultExpireTimeout ?? 5000 - property int notificationsGroupPreviewNum: Config.notifs.groupPreviewNum ?? 3 - - property int maxToasts: Config.utilities.maxToasts ?? 4 - property string toastsFullscreen: Config.utilities.toasts.fullscreen ?? "off" - property bool chargingChanged: GlobalConfig.utilities.toasts.chargingChanged ?? true - property bool gameModeChanged: GlobalConfig.utilities.toasts.gameModeChanged ?? true - property bool dndChanged: GlobalConfig.utilities.toasts.dndChanged ?? true - property bool audioOutputChanged: GlobalConfig.utilities.toasts.audioOutputChanged ?? true - property bool audioInputChanged: GlobalConfig.utilities.toasts.audioInputChanged ?? true - property bool capsLockChanged: GlobalConfig.utilities.toasts.capsLockChanged ?? true - property bool numLockChanged: GlobalConfig.utilities.toasts.numLockChanged ?? true - property bool kbLayoutChanged: GlobalConfig.utilities.toasts.kbLayoutChanged ?? true - property bool vpnChanged: GlobalConfig.utilities.toasts.vpnChanged ?? true - property bool nowPlaying: GlobalConfig.utilities.toasts.nowPlaying ?? false - - function saveConfig(): void { - GlobalConfig.notifs.expire = root.notificationsExpire; - GlobalConfig.notifs.fullscreen = root.notificationsFullscreen; - GlobalConfig.notifs.openExpanded = root.notificationsOpenExpanded; - GlobalConfig.notifs.defaultExpireTimeout = root.notificationsDefaultExpireTimeout; - GlobalConfig.notifs.groupPreviewNum = root.notificationsGroupPreviewNum; - - GlobalConfig.utilities.maxToasts = root.maxToasts; - GlobalConfig.utilities.toasts.fullscreen = root.toastsFullscreen; - GlobalConfig.utilities.toasts.chargingChanged = root.chargingChanged; - GlobalConfig.utilities.toasts.gameModeChanged = root.gameModeChanged; - GlobalConfig.utilities.toasts.dndChanged = root.dndChanged; - GlobalConfig.utilities.toasts.audioOutputChanged = root.audioOutputChanged; - GlobalConfig.utilities.toasts.audioInputChanged = root.audioInputChanged; - GlobalConfig.utilities.toasts.capsLockChanged = root.capsLockChanged; - GlobalConfig.utilities.toasts.numLockChanged = root.numLockChanged; - GlobalConfig.utilities.toasts.kbLayoutChanged = root.kbLayoutChanged; - GlobalConfig.utilities.toasts.vpnChanged = root.vpnChanged; - GlobalConfig.utilities.toasts.nowPlaying = root.nowPlaying; - } - - anchors.fill: parent - - ClippingRectangle { - id: notificationsClippingRect - - anchors.fill: parent - anchors.margins: Tokens.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal - - color: "transparent" - radius: notificationsBorder.innerRadius - - Loader { - id: notificationsLoader - - anchors.fill: parent - anchors.margins: Tokens.padding.large + Tokens.padding.normal - anchors.leftMargin: Tokens.padding.large - anchors.rightMargin: Tokens.padding.large - - sourceComponent: notificationsContentComponent - } - } - - InnerBorder { - id: notificationsBorder - - leftThickness: 0 - rightThickness: Tokens.padding.normal - } - - Component { - id: notificationsContentComponent - - StyledFlickable { - id: notificationsFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: notificationsLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: notificationsFlickable - } - - RowLayout { - id: notificationsLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: Tokens.spacing.normal - - ColumnLayout { - Layout.fillWidth: true - Layout.maximumWidth: 500 - Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal - - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Notifications") - font.pointSize: Tokens.font.size.normal - } - - SplitButtonRow { - id: notificationsFullscreenSelector - - function syncActiveItem(): void { - active = root.notificationsFullscreen === "off" ? notificationsFullscreenOffItem : notificationsFullscreenOnItem; - } - - label: qsTr("Show in fullscreen") - menuItems: [notificationsFullscreenOffItem, notificationsFullscreenOnItem] - - Component.onCompleted: syncActiveItem() - - Connections { - function onNotificationsFullscreenChanged(): void { - notificationsFullscreenSelector.syncActiveItem(); - } - - target: root - } - - MenuItem { - id: notificationsFullscreenOffItem - - text: qsTr("Off") - icon: "notifications_off" - activeText: qsTr("Off") - onClicked: { - root.notificationsFullscreen = "off"; - root.saveConfig(); - } - } - - MenuItem { - id: notificationsFullscreenOnItem - - text: qsTr("On") - icon: "notifications" - activeText: qsTr("On") - onClicked: { - root.notificationsFullscreen = "on"; - root.saveConfig(); - } - } - } - - SwitchRow { - label: qsTr("Expire automatically") - checked: root.notificationsExpire - onToggled: checked => { - root.notificationsExpire = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Open expanded") - checked: root.notificationsOpenExpanded - onToggled: checked => { - root.notificationsOpenExpanded = checked; - root.saveConfig(); - } - } - - SpinBoxRow { - label: qsTr("Default timeout") - value: root.notificationsDefaultExpireTimeout - min: 1000 - max: 60000 - step: 500 - onValueModified: value => { - root.notificationsDefaultExpireTimeout = value; - root.saveConfig(); - } - } - - SpinBoxRow { - label: qsTr("Group preview count") - value: root.notificationsGroupPreviewNum - min: 1 - max: 10 - step: 1 - onValueModified: value => { - root.notificationsGroupPreviewNum = value; - root.saveConfig(); - } - } - } - } - - ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal - - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Toast settings") - font.pointSize: Tokens.font.size.normal - } - - SplitButtonRow { - id: toastFullscreenSelector - - function syncActiveItem(): void { - if (root.toastsFullscreen === "all") { - active = toastFullscreenAllItem; - return; - } - - if (root.toastsFullscreen === "important") { - active = toastFullscreenImportantItem; - return; - } - - active = toastFullscreenOffItem; - } - - Layout.fillWidth: true - z: expanded ? 100 : 0 - label: qsTr("Show in fullscreen") - menuItems: [toastFullscreenOffItem, toastFullscreenImportantItem, toastFullscreenAllItem] - - Component.onCompleted: syncActiveItem() - - Connections { - function onToastsFullscreenChanged(): void { - toastFullscreenSelector.syncActiveItem(); - } - - target: root - } - - MenuItem { - id: toastFullscreenOffItem - - text: qsTr("Off") - icon: "notifications_off" - activeText: qsTr("Off") - onClicked: { - root.toastsFullscreen = "off"; - root.saveConfig(); - } - } - - MenuItem { - id: toastFullscreenImportantItem - - text: qsTr("Important") - icon: "priority_high" - activeText: qsTr("Important") - onClicked: { - root.toastsFullscreen = "important"; - root.saveConfig(); - } - } - - MenuItem { - id: toastFullscreenAllItem - - text: qsTr("On") - icon: "notifications" - activeText: qsTr("On") - onClicked: { - root.toastsFullscreen = "all"; - root.saveConfig(); - } - } - } - - SpinBoxRow { - Layout.fillWidth: true - label: qsTr("Visible toasts") - value: root.maxToasts - min: 1 - max: 10 - step: 1 - onValueModified: value => { - root.maxToasts = value; - root.saveConfig(); - } - } - - GridLayout { - Layout.fillWidth: true - columns: 2 - columnSpacing: Tokens.spacing.normal - rowSpacing: Tokens.spacing.normal - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Charging changes") - checked: root.chargingChanged - onToggled: checked => { - root.chargingChanged = checked; - root.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Game mode changes") - checked: root.gameModeChanged - onToggled: checked => { - root.gameModeChanged = checked; - root.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Do not disturb") - checked: root.dndChanged - onToggled: checked => { - root.dndChanged = checked; - root.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Audio output changes") - checked: root.audioOutputChanged - onToggled: checked => { - root.audioOutputChanged = checked; - root.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Audio input changes") - checked: root.audioInputChanged - onToggled: checked => { - root.audioInputChanged = checked; - root.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Caps lock changes") - checked: root.capsLockChanged - onToggled: checked => { - root.capsLockChanged = checked; - root.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Num lock changes") - checked: root.numLockChanged - onToggled: checked => { - root.numLockChanged = checked; - root.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Keyboard layout changes") - checked: root.kbLayoutChanged - onToggled: checked => { - root.kbLayoutChanged = checked; - root.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("VPN changes") - checked: root.vpnChanged - onToggled: checked => { - root.vpnChanged = checked; - root.saveConfig(); - } - } - - SwitchRow { - Layout.fillWidth: true - label: qsTr("Now playing") - checked: root.nowPlaying - onToggled: checked => { - root.nowPlaying = checked; - root.saveConfig(); - } - } - } - } - } - } - } - } -} diff --git a/modules/controlcenter/state/BluetoothState.qml b/modules/controlcenter/state/BluetoothState.qml deleted file mode 100644 index 9b16bfe5..00000000 --- a/modules/controlcenter/state/BluetoothState.qml +++ /dev/null @@ -1,12 +0,0 @@ -import QtQuick -import Quickshell.Bluetooth - -QtObject { - id: root - - property BluetoothDevice active: null - property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter - property bool editingAdapterName: false - property bool fabMenuOpen: false - property bool editingDeviceName: false -} diff --git a/modules/controlcenter/state/EthernetState.qml b/modules/controlcenter/state/EthernetState.qml deleted file mode 100644 index 58f5fc8c..00000000 --- a/modules/controlcenter/state/EthernetState.qml +++ /dev/null @@ -1,7 +0,0 @@ -import QtQuick - -QtObject { - id: root - - property var active: null -} diff --git a/modules/controlcenter/state/LauncherState.qml b/modules/controlcenter/state/LauncherState.qml deleted file mode 100644 index 58f5fc8c..00000000 --- a/modules/controlcenter/state/LauncherState.qml +++ /dev/null @@ -1,7 +0,0 @@ -import QtQuick - -QtObject { - id: root - - property var active: null -} diff --git a/modules/controlcenter/state/NetworkState.qml b/modules/controlcenter/state/NetworkState.qml deleted file mode 100644 index f9324c83..00000000 --- a/modules/controlcenter/state/NetworkState.qml +++ /dev/null @@ -1,9 +0,0 @@ -import QtQuick - -QtObject { - id: root - - property var active: null - property bool showPasswordDialog: false - property var pendingNetwork: null -} diff --git a/modules/controlcenter/state/VpnState.qml b/modules/controlcenter/state/VpnState.qml deleted file mode 100644 index aa911f11..00000000 --- a/modules/controlcenter/state/VpnState.qml +++ /dev/null @@ -1,5 +0,0 @@ -import QtQuick - -QtObject { - property var active: null -} diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml deleted file mode 100644 index 601fc09f..00000000 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ /dev/null @@ -1,793 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.components.controls -import qs.components.effects -import qs.services -import qs.utils - -Item { - id: root - - required property Session session - - property bool activeWindowCompact: Config.bar.activeWindow.compact ?? false - property bool activeWindowInverted: Config.bar.activeWindow.inverted ?? false - property bool clockShowIcon: Config.bar.clock.showIcon ?? true - property bool clockBackground: Config.bar.clock.background ?? false - property bool clockShowDate: Config.bar.clock.showDate ?? false - property bool persistent: Config.bar.persistent ?? true - property bool showOnHover: Config.bar.showOnHover ?? true - property int dragThreshold: Config.bar.dragThreshold ?? 20 - property bool showAudio: Config.bar.status.showAudio ?? true - property bool showMicrophone: Config.bar.status.showMicrophone ?? true - property bool showKbLayout: Config.bar.status.showKbLayout ?? false - property bool showNetwork: Config.bar.status.showNetwork ?? true - property bool showWifi: Config.bar.status.showWifi ?? true - property bool showBluetooth: Config.bar.status.showBluetooth ?? true - property bool showBattery: Config.bar.status.showBattery ?? true - property bool showLockStatus: Config.bar.status.showLockStatus ?? true - property bool trayBackground: Config.bar.tray.background ?? false - property bool trayCompact: Config.bar.tray.compact ?? false - property bool trayRecolour: Config.bar.tray.recolour ?? false - property int workspacesShown: Config.bar.workspaces.shown ?? 5 - property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true - property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false - property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false - property int workspacesMaxWindowIcons: Config.bar.workspaces.maxWindowIcons ?? 0 - property bool workspacesPerMonitor: GlobalConfig.bar.workspaces.perMonitorWorkspaces ?? true - property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true - property bool scrollVolume: Config.bar.scrollActions.volume ?? true - property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true - property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true - property bool popoutTray: Config.bar.popouts.tray ?? true - property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true - property list monitorNames: Hypr.monitorNames() - property list excludedScreens: Config.bar.excludedScreens ?? [] - - function saveConfig(entryIndex, entryEnabled) { - GlobalConfig.bar.activeWindow.compact = root.activeWindowCompact; - GlobalConfig.bar.activeWindow.inverted = root.activeWindowInverted; - GlobalConfig.bar.clock.background = root.clockBackground; - GlobalConfig.bar.clock.showDate = root.clockShowDate; - GlobalConfig.bar.clock.showIcon = root.clockShowIcon; - GlobalConfig.bar.persistent = root.persistent; - GlobalConfig.bar.showOnHover = root.showOnHover; - GlobalConfig.bar.dragThreshold = root.dragThreshold; - GlobalConfig.bar.status.showAudio = root.showAudio; - GlobalConfig.bar.status.showMicrophone = root.showMicrophone; - GlobalConfig.bar.status.showKbLayout = root.showKbLayout; - GlobalConfig.bar.status.showNetwork = root.showNetwork; - GlobalConfig.bar.status.showWifi = root.showWifi; - GlobalConfig.bar.status.showBluetooth = root.showBluetooth; - GlobalConfig.bar.status.showBattery = root.showBattery; - GlobalConfig.bar.status.showLockStatus = root.showLockStatus; - GlobalConfig.bar.tray.background = root.trayBackground; - GlobalConfig.bar.tray.compact = root.trayCompact; - GlobalConfig.bar.tray.recolour = root.trayRecolour; - GlobalConfig.bar.workspaces.shown = root.workspacesShown; - GlobalConfig.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; - GlobalConfig.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; - GlobalConfig.bar.workspaces.showWindows = root.workspacesShowWindows; - GlobalConfig.bar.workspaces.maxWindowIcons = root.workspacesMaxWindowIcons; - GlobalConfig.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; - GlobalConfig.bar.scrollActions.workspaces = root.scrollWorkspaces; - GlobalConfig.bar.scrollActions.volume = root.scrollVolume; - GlobalConfig.bar.scrollActions.brightness = root.scrollBrightness; - GlobalConfig.bar.popouts.activeWindow = root.popoutActiveWindow; - GlobalConfig.bar.popouts.tray = root.popoutTray; - GlobalConfig.bar.popouts.statusIcons = root.popoutStatusIcons; - GlobalConfig.bar.excludedScreens = root.excludedScreens; - - const entries = []; - for (let i = 0; i < entriesModel.count; i++) { - const entry = entriesModel.get(i); - let enabled = entry.enabled; - if (entryIndex !== undefined && i === entryIndex) { - enabled = entryEnabled; - } - entries.push({ - id: entry.id, - enabled: enabled - }); - } - GlobalConfig.bar.entries = entries; - } - - anchors.fill: parent - - Component.onCompleted: { - if (Config.bar.entries) { - entriesModel.clear(); - for (let i = 0; i < Config.bar.entries.length; i++) { - const entry = Config.bar.entries[i]; - entriesModel.append({ - id: entry.id, - enabled: entry.enabled !== false - }); - } - } - } - - ListModel { - id: entriesModel - } - - ClippingRectangle { - id: taskbarClippingRect - - anchors.fill: parent - anchors.margins: Tokens.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal - - radius: taskbarBorder.innerRadius - color: "transparent" - - Loader { - id: taskbarLoader - - anchors.fill: parent - anchors.margins: Tokens.padding.large + Tokens.padding.normal - anchors.leftMargin: Tokens.padding.large - anchors.rightMargin: Tokens.padding.large - - asynchronous: true - sourceComponent: taskbarContentComponent - } - } - - InnerBorder { - id: taskbarBorder - - leftThickness: 0 - rightThickness: Tokens.padding.normal - } - - Component { - id: taskbarContentComponent - - StyledFlickable { - id: sidebarFlickable - - flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: sidebarFlickable - } - - ColumnLayout { - id: sidebarLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - - spacing: Tokens.spacing.normal - - RowLayout { - spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Taskbar") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - } - - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Status Icons") - font.pointSize: Tokens.font.size.normal - } - - ConnectedButtonGroup { - rootItem: root - - options: [ - { - label: qsTr("Speakers"), - propertyName: "showAudio", - onToggled: function (checked) { - root.showAudio = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Microphone"), - propertyName: "showMicrophone", - onToggled: function (checked) { - root.showMicrophone = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Keyboard"), - propertyName: "showKbLayout", - onToggled: function (checked) { - root.showKbLayout = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Network"), - propertyName: "showNetwork", - onToggled: function (checked) { - root.showNetwork = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Wifi"), - propertyName: "showWifi", - onToggled: function (checked) { - root.showWifi = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Bluetooth"), - propertyName: "showBluetooth", - onToggled: function (checked) { - root.showBluetooth = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Battery"), - propertyName: "showBattery", - onToggled: function (checked) { - root.showBattery = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Capslock"), - propertyName: "showLockStatus", - onToggled: function (checked) { - root.showLockStatus = checked; - root.saveConfig(); - } - } - ] - } - } - - RowLayout { - id: mainRowLayout - - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - ColumnLayout { - id: leftColumnLayout - - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal - - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Workspaces") - font.pointSize: Tokens.font.size.normal - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesShownRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesShownRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Shown") - } - - CustomSpinBox { - min: 1 - max: 20 - value: root.workspacesShown - onValueModified: value => { - root.workspacesShown = value; - root.saveConfig(); - } - } - } - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesActiveIndicatorRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Active indicator") - } - - StyledSwitch { - checked: root.workspacesActiveIndicator - onToggled: { - root.workspacesActiveIndicator = checked; - root.saveConfig(); - } - } - } - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesOccupiedBgRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesOccupiedBgRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Occupied background") - } - - StyledSwitch { - checked: root.workspacesOccupiedBg - onToggled: { - root.workspacesOccupiedBg = checked; - root.saveConfig(); - } - } - } - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesShowWindowsRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesShowWindowsRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show windows") - } - - StyledSwitch { - checked: root.workspacesShowWindows - onToggled: { - root.workspacesShowWindows = checked; - root.saveConfig(); - } - } - } - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesMaxWindowIconsRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesMaxWindowIconsRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Max window icons") - } - - CustomSpinBox { - min: 0 - max: 20 - value: root.workspacesMaxWindowIcons - onValueModified: value => { - root.workspacesMaxWindowIcons = value; - root.saveConfig(); - } - } - } - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesPerMonitorRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesPerMonitorRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Per monitor workspaces") - } - - StyledSwitch { - checked: root.workspacesPerMonitor - onToggled: { - root.workspacesPerMonitor = checked; - root.saveConfig(); - } - } - } - } - } - - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Scroll Actions") - font.pointSize: Tokens.font.size.normal - } - - ConnectedButtonGroup { - rootItem: root - - options: [ - { - label: qsTr("Workspaces"), - propertyName: "scrollWorkspaces", - onToggled: function (checked) { - root.scrollWorkspaces = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Volume"), - propertyName: "scrollVolume", - onToggled: function (checked) { - root.scrollVolume = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Brightness"), - propertyName: "scrollBrightness", - onToggled: function (checked) { - root.scrollBrightness = checked; - root.saveConfig(); - } - } - ] - } - } - } - - ColumnLayout { - id: middleColumnLayout - - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal - - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Clock") - font.pointSize: Tokens.font.size.normal - } - - SwitchRow { - label: qsTr("Background") - checked: root.clockBackground - onToggled: checked => { - root.clockBackground = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show date") - checked: root.clockShowDate - onToggled: checked => { - root.clockShowDate = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show clock icon") - checked: root.clockShowIcon - onToggled: checked => { - root.clockShowIcon = checked; - root.saveConfig(); - } - } - } - - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Bar Behavior") - font.pointSize: Tokens.font.size.normal - } - - SwitchRow { - label: qsTr("Persistent") - checked: root.persistent - onToggled: checked => { - root.persistent = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show on hover") - checked: root.showOnHover - onToggled: checked => { - root.showOnHover = checked; - root.saveConfig(); - } - } - - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Drag threshold") - value: root.dragThreshold - from: 0 - to: 100 - suffix: "px" - validator: IntValidator { - bottom: 0 - top: 100 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) - - onValueModified: newValue => { - root.dragThreshold = Math.round(newValue); - root.saveConfig(); - } - } - } - } - - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Active window") - font.pointSize: Tokens.font.size.normal - } - - SwitchRow { - label: qsTr("Compact") - checked: root.activeWindowCompact - onToggled: checked => { - root.activeWindowCompact = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Inverted") - checked: root.activeWindowInverted - onToggled: checked => { - root.activeWindowInverted = checked; - root.saveConfig(); - } - } - } - } - - ColumnLayout { - id: rightColumnLayout - - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal - - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Popouts") - font.pointSize: Tokens.font.size.normal - } - - SwitchRow { - label: qsTr("Active window") - checked: root.popoutActiveWindow - onToggled: checked => { - root.popoutActiveWindow = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Tray") - checked: root.popoutTray - onToggled: checked => { - root.popoutTray = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Status icons") - checked: root.popoutStatusIcons - onToggled: checked => { - root.popoutStatusIcons = checked; - root.saveConfig(); - } - } - } - - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Tray Settings") - font.pointSize: Tokens.font.size.normal - } - - ConnectedButtonGroup { - rootItem: root - - options: [ - { - label: qsTr("Background"), - propertyName: "trayBackground", - onToggled: function (checked) { - root.trayBackground = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Compact"), - propertyName: "trayCompact", - onToggled: function (checked) { - root.trayCompact = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Recolour"), - propertyName: "trayRecolour", - onToggled: function (checked) { - root.trayRecolour = checked; - root.saveConfig(); - } - } - ] - } - } - - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Monitors") - font.pointSize: Tokens.font.size.normal - } - - ConnectedButtonGroup { - rootItem: root - // max 3 options per line - rows: Math.ceil(root.monitorNames.length / 3) - - options: root.monitorNames.map(e => ({ - label: qsTr(e), - propertyName: `monitor${e}`, - onToggled: function (_) { - // if the given monitor is in the excluded list, it should be added back - let addedBack = excludedScreens.includes(e); - if (addedBack) { - const index = excludedScreens.indexOf(e); - if (index !== -1) { - excludedScreens.splice(index, 1); - } - } else { - if (!excludedScreens.includes(e)) { - excludedScreens.push(e); - } - } - root.saveConfig(); - }, - state: !Strings.testRegexList(root.excludedScreens, e) - })) - } - } - } - } - } - } - } -} diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 95d2a08a..0a1e23c2 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -4,6 +4,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets +import Caelestia import Caelestia.Config import qs.components import qs.components.filedialog @@ -12,15 +13,6 @@ Item { id: root required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: { - const count = repeater.count; - for (let i = 0; i < count; i++) { - const item = repeater.itemAt(i) as Loader; - if (item?.sourceComponent === mediaComponent && (item?.item as MediaWrapper)?.needsKeyboard) - return true; - } - return false; - } required property DashboardState dashState required property FileDialog facePicker @@ -42,7 +34,7 @@ Item { component: performanceComponent, iconName: "speed", text: qsTr("Performance"), - enabled: Config.dashboard.showPerformance && (Config.dashboard.performance.showCpu || Config.dashboard.performance.showGpu || Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork || Config.dashboard.performance.showBattery) + enabled: Config.dashboard.showPerformance }, { component: weatherComponent, @@ -66,7 +58,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: Tokens.padding.normal + anchors.topMargin: CUtils.clamp(anchors.margins - Config.border.thickness, 0, anchors.margins) anchors.margins: Tokens.padding.large nonAnimWidth: root.nonAnimWidth - anchors.margins * 2 @@ -83,7 +75,7 @@ Item { anchors.bottom: parent.bottom anchors.margins: Tokens.padding.large - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: "transparent" Flickable { @@ -174,7 +166,7 @@ Item { Component { id: mediaComponent - MediaWrapper { + Media { visibilities: root.visibilities } } @@ -198,14 +190,10 @@ Item { } Behavior on implicitWidth { - Anim { - type: Anim.EmphasizedLarge - } + Anim {} } Behavior on implicitHeight { - Anim { - type: Anim.EmphasizedLarge - } + Anim {} } } diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index 6785ede8..02e16231 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -12,16 +12,16 @@ GridLayout { required property DashboardState dashState required property FileDialog facePicker - rowSpacing: Tokens.spacing.normal - columnSpacing: Tokens.spacing.normal + rowSpacing: Tokens.spacing.medium + columnSpacing: Tokens.spacing.medium Rect { Layout.column: 2 Layout.columnSpan: 3 - Layout.preferredWidth: user.implicitWidth - Layout.preferredHeight: user.implicitHeight + Layout.preferredWidth: Tokens.sizes.dashboard.userWidth + Layout.fillHeight: true - radius: Tokens.rounding.large + radius: Tokens.rounding.extraLarge User { id: user @@ -35,11 +35,13 @@ GridLayout { Layout.row: 0 Layout.columnSpan: 2 Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth - Layout.fillHeight: true + Layout.preferredHeight: weather.implicitHeight - radius: Tokens.rounding.large * 1.5 + radius: Tokens.rounding.extraLarge * 1.5 - SmallWeather {} + SmallWeather { + id: weather + } } Rect { @@ -47,7 +49,7 @@ GridLayout { Layout.preferredWidth: dateTime.implicitWidth Layout.fillHeight: true - radius: Tokens.rounding.normal + radius: Tokens.rounding.large DateTime { id: dateTime @@ -61,7 +63,7 @@ GridLayout { Layout.fillWidth: true Layout.preferredHeight: calendar.implicitHeight - radius: Tokens.rounding.large + radius: Tokens.rounding.extraLarge Calendar { id: calendar @@ -76,7 +78,7 @@ GridLayout { Layout.preferredWidth: resources.implicitWidth Layout.fillHeight: true - radius: Tokens.rounding.normal + radius: Tokens.rounding.large Resources { id: resources @@ -90,7 +92,7 @@ GridLayout { Layout.preferredWidth: media.implicitWidth Layout.fillHeight: true - radius: Tokens.rounding.large * 2 + radius: Tokens.rounding.extraLarge * 2 Media { id: media diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml deleted file mode 100644 index 1e5461f5..00000000 --- a/modules/dashboard/LyricMenu.qml +++ /dev/null @@ -1,412 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.services - -StyledRect { - id: root - - required property real contentHeight - - function searchCandidates(title, artist) { - LyricsService.currentRequestId++; - LyricsService.fetchNetEaseCandidates(title, artist, LyricsService.currentRequestId); - } - - implicitHeight: contentHeight - - radius: Tokens.rounding.large - color: Colours.tPalette.m3surfaceContainer - - Loader { - asynchronous: true - anchors.fill: parent - active: root.height > 0 - - sourceComponent: ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - // Header: icon, backend selector, refresh, toggle - RowLayout { - Layout.fillWidth: true - spacing: Tokens.padding.small - - MaterialIcon { - text: "lyrics" - fill: 1 - color: Colours.palette.m3primary - font.pointSize: Tokens.spacing.large - } - - Rectangle { - Layout.preferredHeight: 24 - Layout.preferredWidth: 80 - radius: Tokens.rounding.small - color: Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.15) - - StyledText { - anchors.centerIn: parent - text: LyricsService.preferredBackend - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3primary - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - const backends = ["Auto", "Local", "NetEase"]; - const currentIndex = backends.indexOf(LyricsService.preferredBackend); - const nextIndex = (currentIndex + 1) % backends.length; - LyricsService.preferredBackend = backends[nextIndex]; - LyricsService.loadLyrics(); - } - } - } - - Rectangle { - Layout.preferredHeight: 24 - Layout.preferredWidth: 60 - radius: Tokens.rounding.small - visible: LyricsService.preferredBackend === "Auto" - color: LyricsService.backend === "Local" ? Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.15) : Qt.rgba(Colours.palette.m3secondary.r, Colours.palette.m3secondary.g, Colours.palette.m3secondary.b, 0.15) - - StyledText { - anchors.centerIn: parent - text: LyricsService.backend - font.pointSize: Tokens.font.size.small - color: LyricsService.backend === "Local" ? Colours.palette.m3tertiary : Colours.palette.m3secondary - } - } - - Item { - Layout.fillWidth: true - } - - IconButton { - icon: "refresh" - type: IconButton.Text - onClicked: LyricsService.loadLyrics() - } - - StyledSwitch { - checked: LyricsService.lyricsVisible - onToggled: LyricsService.toggleVisibility() - } - } - - StyledText { - Layout.fillWidth: true - text: LyricsService.preferredBackend === "Local" ? "Loaded File:" : "Fetched Candidates:" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - elide: Text.ElideRight - visible: LyricsService.preferredBackend === "Local" ? LyricsService.loadedLocalFile.length > 0 : LyricsService.candidatesModel.count > 0 - } - - // Local file info (shown in Local mode) - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 48 - visible: LyricsService.preferredBackend === "Local" && LyricsService.loadedLocalFile.length > 0 - radius: Tokens.rounding.small - color: Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.1) - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.small - spacing: 0 - - StyledText { - Layout.fillWidth: true - text: { - const path = LyricsService.loadedLocalFile; - const parts = path.split('/'); - return parts[parts.length - 1]; - } - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3tertiary - elide: Text.ElideMiddle - } - - StyledText { - Layout.fillWidth: true - text: { - const path = LyricsService.loadedLocalFile; - const parts = path.split('/'); - if (parts.length > 2) { - return parts.slice(-3, -1).join('/'); - } - return ""; - } - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - elide: Text.ElideMiddle - } - } - } - - // Candidates list - Loader { - Layout.fillWidth: true - Layout.fillHeight: true - - active: LyricsService.preferredBackend !== "Local" - - sourceComponent: ListView { - id: candidatesView - - model: LyricsService.candidatesModel - clip: true - spacing: Tokens.spacing.small - visible: LyricsService.candidatesModel.count > 0 - opacity: visible ? 1 : 0 - - delegate: Item { - id: delegateRoot - - required property real id - required property string title - required property string artist - - property bool hovered: false - property bool pressed: false - - width: ListView.view.width * 0.98 - height: 70 - - anchors.horizontalCenter: parent?.horizontalCenter - scale: hovered ? 1.02 : 1.0 - - Behavior on scale { - NumberAnimation { - duration: Tokens.anim.durations.small - easing.type: Easing.OutCubic - } - } - - Rectangle { - id: background - - anchors.fill: parent - radius: Tokens.rounding.small - - color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03) - - border.width: delegateRoot.hovered ? 1 : 0 - border.color: Colours.palette.m3primary - - Behavior on color { - ColorAnimation { - duration: Tokens.anim.durations.small - } - } - Behavior on border.width { - NumberAnimation { - duration: Tokens.anim.durations.small - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onEntered: delegateRoot.hovered = true - onExited: delegateRoot.hovered = false - onPressed: delegateRoot.pressed = true - onReleased: delegateRoot.pressed = false - onClicked: LyricsService.selectCandidate(delegateRoot.id) - } - - Row { - anchors.fill: parent - anchors.margins: Tokens.padding.normal - spacing: Tokens.spacing.small - - // Active indicator bar - Rectangle { - width: 4 - height: parent.height * 0.6 - radius: 2 - anchors.verticalCenter: parent.verticalCenter - color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent" - - Behavior on color { - ColorAnimation { - duration: Tokens.anim.durations.small - } - } - } - - Column { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 30 - spacing: 4 - - Text { - text: delegateRoot.title - font.pointSize: Tokens.font.size.normal - font.bold: true - color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface - width: parent.width - elide: Text.ElideRight - - Behavior on color { - ColorAnimation { - duration: Tokens.anim.durations.small - } - } - } - - Text { - text: delegateRoot.artist - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - elide: Text.ElideRight - } - } - } - } - } - } - - Item { - Layout.fillHeight: true - visible: LyricsService.candidatesModel.count == 0 && LyricsService.preferredBackend !== "Local" - } - - // Manual search - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.padding.small - - StyledText { - Layout.fillWidth: true - text: "Manual Search" - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - elide: Text.ElideRight - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.padding.small - - StyledInputField { - id: searchTitle - - Layout.fillWidth: true - horizontalAlignment: TextInput.AlignLeft - - Binding { - target: searchTitle - property: "text" - value: (Players.active?.trackTitle ?? qsTr("title")) || qsTr("title") - } - } - - StyledInputField { - id: searchArtist - - Layout.fillWidth: true - horizontalAlignment: TextInput.AlignLeft - - Binding { - target: searchArtist - property: "text" - value: (Players.active?.trackArtist ?? qsTr("artist")) || qsTr("artist") - } - } - - IconButton { - icon: "search" - onClicked: root.searchCandidates(searchTitle.text, searchArtist.text) - } - } - } - - // Offset controls - RowLayout { - Layout.fillWidth: true - spacing: Tokens.padding.small - - MaterialIcon { - text: "contrast_square" - font.pointSize: Tokens.font.size.large - color: Colours.palette.m3secondary - } - - StyledText { - text: "Offset" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - IconButton { - icon: "remove" - type: IconButton.Text - onClicked: { - LyricsService.offset = parseFloat((LyricsService.offset - 0.1).toFixed(1)); - LyricsService.savePrefs(); - } - } - - TextInput { - id: offsetInput - - horizontalAlignment: TextInput.AlignHCenter - color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.normal - selectByMouse: true - text: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" - onEditingFinished: { - let cleaned = offsetInput.text.replace(/[+s]/g, "").trim(); - let val = parseFloat(cleaned); - if (!isNaN(val)) { - LyricsService.offset = parseFloat(val.toFixed(1)); - LyricsService.savePrefs(); - } else { - offsetInput.text = (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s"; - } - } - - Binding { - target: offsetInput - property: "text" - value: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" - when: !offsetInput.activeFocus - } - - Connections { - function onCurrentRequestIdChanged() { - offsetInput.focus = false; - } - - target: LyricsService - } - } - - IconButton { - icon: "add" - type: IconButton.Text - onClicked: { - LyricsService.offset = parseFloat((LyricsService.offset + 0.1).toFixed(1)); - LyricsService.savePrefs(); - } - } - } - } - } -} diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml deleted file mode 100644 index c0188304..00000000 --- a/modules/dashboard/LyricsView.qml +++ /dev/null @@ -1,112 +0,0 @@ -import QtQuick -import QtQuick.Effects -import Quickshell -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.services - -StyledListView { - id: root - - readonly property bool lyricsActuallyVisible: LyricsService.lyricsVisible && LyricsService.model.count != 0 - - clip: true - model: LyricsService.model - currentIndex: LyricsService.currentIndex - visible: lyricsActuallyVisible || hideTimer.running - preferredHighlightBegin: height / 2 - 30 - preferredHighlightEnd: height / 2 + 30 - highlightRangeMode: ListView.ApplyRange - highlightFollowsCurrentItem: true - highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Tokens.anim.durations.normal - layer.enabled: true - layer.effect: ShaderEffect { - required property Item source - property real fadeMargin: 0.5 - - fragmentShader: Quickshell.shellPath("assets/shaders/fade.frag.qsb") - } - onLyricsActuallyVisibleChanged: { - if (!lyricsActuallyVisible) - hideTimer.restart(); - } - onModelChanged: { - if (model && model.count > 0) { - Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)); - } - } - delegate: Item { - id: delegateRoot - - required property string lyricLine - required property real time - required property int index - readonly property bool hasContent: lyricLine && lyricLine.trim().length > 0 - property bool isCurrent: ListView.isCurrentItem - - width: ListView.view.width - height: hasContent ? (lyricText.contentHeight + Tokens.spacing.large) : 0 - - MultiEffect { - id: effect - - anchors.fill: lyricText - source: lyricText - scale: lyricText.scale - enabled: delegateRoot.isCurrent - visible: delegateRoot.isCurrent - - blurEnabled: true - blur: 0.4 - - shadowEnabled: true - shadowColor: Colours.palette.m3primary - shadowOpacity: 0.5 - shadowBlur: 0.6 - shadowHorizontalOffset: 0 - shadowVerticalOffset: 0 - - autoPaddingEnabled: true - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: LyricsService.jumpTo(delegateRoot.index, delegateRoot.time) - } - - Text { - id: lyricText - - text: delegateRoot.lyricLine ? delegateRoot.lyricLine.replace(/\u00A0/g, " ") : "" - width: parent.width * 0.85 - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - font.pointSize: Tokens.font.size.normal - color: delegateRoot.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - font.bold: delegateRoot.isCurrent - scale: delegateRoot.isCurrent ? 1.15 : 1.0 - - Behavior on color { - CAnim { - duration: Tokens.anim.durations.small - } - } - Behavior on scale { - Anim { - type: Anim.StandardSmall - } - } - } - } - - Timer { - id: hideTimer - - interval: 300 // long enough to bridge the track switch gap - running: false - repeat: false - } -} diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 367d9e98..2a2f5c2e 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -1,513 +1,150 @@ -pragma ComponentBehavior: Bound - +import "media" import QtQuick import QtQuick.Layouts -import QtQuick.Shapes -import Quickshell -import Quickshell.Services.Mpris +import M3Shapes import Caelestia.Config -import Caelestia.Services import qs.components -import qs.components.controls import qs.services -import qs.utils Item { id: root required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: lyricMenuOpen - readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Tokens.sizes.dashboard.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Tokens.padding.large * 2 - readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight + implicitWidth: Tokens.sizes.dashboard.mediaTabWidth + implicitHeight: Tokens.sizes.dashboard.mediaTabHeight - property bool lyricMenuOpen: false - property bool lyricsShowing: LyricsService.lyricsVisible && LyricsService.model.count != 0 - property bool lyricsShowingDebounced: false - - property real playerProgress: { - const active = Players.active; - return active?.length ? (active.position % active.length) / active.length : 0; - } - - function lengthStr(length: int): string { - if (length < 0) - return "-1:-1"; - - const hours = Math.floor(length / 3600); - const mins = Math.floor((length % 3600) / 60); - const secs = Math.floor(length % 60).toString().padStart(2, "0"); - - if (hours > 0) - return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`; - return `${mins}:${secs}`; - } - - onLyricsShowingChanged: { - if (lyricsShowing) { - lyricsHideDelay.stop(); - lyricsShowingDebounced = true; - } else { - lyricsHideDelay.restart(); - } - } - - implicitWidth: cover.implicitWidth + Tokens.sizes.dashboard.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Tokens.padding.large * 2 - implicitHeight: nonAnimHeight - - Behavior on implicitHeight { - Anim {} - } - - Behavior on playerProgress { - Anim { - type: Anim.StandardLarge - } - } - - Timer { - running: Players.active?.isPlaying ?? false - interval: GlobalConfig.dashboard.mediaUpdateInterval - triggeredOnStart: true - repeat: true - onTriggered: { - if (!Players.active) - return; - LyricsService.updatePosition(); - Players.active?.positionChanged(); - } - } - - Timer { - id: lyricsHideDelay - - interval: 300 - repeat: false - } - - Connections { - function onTriggered() { - root.lyricsShowingDebounced = false; - } - - target: lyricsHideDelay - } - - ServiceRef { - service: Audio.cava - } - - ServiceRef { - service: Audio.beatTracker - } - - Shape { - id: visualiser - - readonly property real centerX: width / 2 - readonly property real centerY: height / 2 - readonly property real innerX: cover.implicitWidth / 2 + Tokens.spacing.small - readonly property real innerY: cover.implicitHeight / 2 + Tokens.spacing.small - property color colour: Colours.palette.m3primary - - anchors.fill: cover - anchors.margins: -Tokens.sizes.dashboard.mediaVisualiserSize - - asynchronous: true - preferredRendererType: Shape.CurveRenderer - data: visualiserBars.instances - } - - Variants { - id: visualiserBars - - model: Array.from({ - length: GlobalConfig.services.visualiserBars - }, (_, i) => i) - - ShapePath { - id: visualiserBar - - required property int modelData - readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData])) - - readonly property real angle: modelData * 2 * Math.PI / GlobalConfig.services.visualiserBars - readonly property real magnitude: value * root.Tokens.sizes.dashboard.mediaVisualiserSize - readonly property real cos: Math.cos(angle) - readonly property real sin: Math.sin(angle) - - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - strokeWidth: 360 / GlobalConfig.services.visualiserBars - root.Tokens.spacing.small / 4 - strokeColor: Colours.palette.m3primary - - startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos - startY: visualiser.centerY + (visualiser.innerY + strokeWidth / 2) * sin - - PathLine { - x: visualiser.centerX + (visualiser.innerX + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.cos - y: visualiser.centerY + (visualiser.innerY + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.sin - } - - Behavior on strokeColor { - CAnim {} - } - } - } - - StyledClippingRect { - id: cover - - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: Tokens.padding.large + Tokens.sizes.dashboard.mediaVisualiserSize - - implicitWidth: Tokens.sizes.dashboard.mediaCoverArtSize - implicitHeight: Tokens.sizes.dashboard.mediaCoverArtSize - - color: Colours.tPalette.m3surfaceContainerHigh - radius: Infinity - - MaterialIcon { - anchors.centerIn: parent - - grade: 200 - text: "art_track" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: (parent.width * 0.4) || 1 - } - - Image { - id: image - - anchors.fill: parent - - source: Players.getArtUrl(Players.active) - asynchronous: true - fillMode: Image.PreserveAspectCrop - sourceSize: { - const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; - return Qt.size(width * dpr, height * dpr); - } - - MouseArea { - anchors.fill: parent - onClicked: { - LyricsService.toggleVisibility(); - } - } - } - } - - ColumnLayout { - id: details - - anchors.verticalCenter: parent.verticalCenter - anchors.left: visualiser.right - anchors.leftMargin: Tokens.spacing.normal - - spacing: Tokens.spacing.small - - StyledText { - id: title - - Layout.fillWidth: true - Layout.maximumWidth: parent.implicitWidth - - animate: true - horizontalAlignment: Text.AlignHCenter - text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") - color: Players.active ? Colours.palette.m3primary : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - - StyledText { - id: album - - Layout.fillWidth: true - Layout.maximumWidth: parent.implicitWidth - - animate: true - horizontalAlignment: Text.AlignHCenter - visible: !!Players.active - text: Players.active?.trackAlbum || qsTr("Unknown album") - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - elide: Text.ElideRight - } - - StyledText { - id: artist - - Layout.fillWidth: true - Layout.maximumWidth: parent.implicitWidth - - animate: true - horizontalAlignment: Text.AlignHCenter - text: (Players.active?.trackArtist ?? qsTr("Play some music for stuff to show up here!")) || qsTr("Unknown artist") - color: Players.active ? Colours.palette.m3secondary : Colours.palette.m3outline - elide: Text.ElideRight - wrapMode: Players.active ? Text.NoWrap : Text.WordWrap - } - - LyricsView { - id: lyricsViewInDetails - - Layout.fillWidth: true - Layout.preferredHeight: 200 - } - - RowLayout { - id: controls - - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Tokens.spacing.small - Layout.bottomMargin: Tokens.spacing.smaller - - spacing: Tokens.spacing.small - - PlayerControl { - type: IconButton.Text - icon: Players.active?.shuffle ? "shuffle_on" : "shuffle" - font.pointSize: Math.round(Tokens.font.size.large) - disabled: !Players.active?.shuffleSupported - onClicked: Players.active.shuffle = !Players.active?.shuffle - } - - PlayerControl { - type: IconButton.Text - icon: "skip_previous" - font.pointSize: Math.round(Tokens.font.size.large * 1.5) - disabled: !Players.active?.canGoPrevious - onClicked: Players.active?.previous() - } - - PlayerControl { - icon: Players.active?.isPlaying ? "pause" : "play_arrow" - label.animate: true - toggle: true - padding: Tokens.padding.small / 2 - checked: Players.active?.isPlaying ?? false - font.pointSize: Math.round(Tokens.font.size.large * 1.5) - disabled: !Players.active?.canTogglePlaying - onClicked: Players.active?.togglePlaying() - } - - PlayerControl { - type: IconButton.Text - icon: "skip_next" - font.pointSize: Math.round(Tokens.font.size.large * 1.5) - disabled: !Players.active?.canGoNext - onClicked: Players.active?.next() - } - - PlayerControl { - type: IconButton.Text - icon: "lyrics" - font.pointSize: Math.round(Tokens.font.size.large) - onClicked: root.lyricMenuOpen = !root.lyricMenuOpen - } - } - - StyledSlider { - id: slider - - enabled: !!Players.active - implicitWidth: 280 - implicitHeight: Tokens.padding.normal * 3 - - onMoved: { - const active = Players.active; - if (active?.canSeek && active?.positionSupported) - active.position = value * active.length; - } - - Binding { - target: slider - property: "value" - value: root.playerProgress - when: !slider.pressed - } - - CustomMouseArea { - function onWheel(event: WheelEvent) { - const active = Players.active; - if (!active?.canSeek || !active?.positionSupported) - return; - - event.accepted = true; - const delta = event.angleDelta.y > 0 ? 10 : -10; // Time 10 seconds - Qt.callLater(() => { - active.position = Math.max(0, Math.min(active.length, active.position + delta)); - }); - } - - anchors.fill: parent - acceptedButtons: Qt.NoButton - } - } - - Item { - Layout.fillWidth: true - implicitHeight: Math.max(position.implicitHeight, length.implicitHeight) - - StyledText { - id: position - - anchors.left: parent.left - - text: root.lengthStr(Players.active ? Players.active.position % Players.active.length : -1) - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - } - - StyledText { - id: length - - anchors.right: parent.right - - text: root.lengthStr(Players.active?.length ?? -1) - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - } - } - } - - ColumnLayout { - id: leftSection - - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0 - anchors.left: details.right - anchors.leftMargin: Tokens.spacing.normal - - visible: lyricMenu.height === 0 || opacity > 0 - opacity: lyricMenu.height === 0 ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: Tokens.anim.durations.normal - easing.type: Easing.OutCubic - } - } - - Item { - id: bongocat - - implicitWidth: visualiser.width - implicitHeight: visualiser.height - - AnimatedImage { - anchors.centerIn: parent - - width: visualiser.width * 0.75 - height: visualiser.height * 0.75 - - playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type - source: Paths.absolutePath(Config.paths.mediaGif) - asynchronous: true - fillMode: AnimatedImage.PreserveAspectFit - } - } - } - - LyricMenu { - id: lyricMenu - - anchors.top: parent.top - anchors.left: details.right - anchors.right: parent.right - anchors.leftMargin: Tokens.spacing.normal - - contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Tokens.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight - - visible: root.lyricMenuOpen || height > 0 - height: root.lyricMenuOpen ? implicitHeight : 0 - clip: true - - Behavior on height { - NumberAnimation { - duration: Tokens.anim.durations.normal - easing.type: Easing.OutCubic - } - } + BackgroundShapes { + anchors.fill: parent } RowLayout { - id: playerChanger + anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.extraLarge - parent: !root.lyricsShowingDebounced ? details : leftSection - Layout.alignment: Qt.AlignHCenter - spacing: Tokens.spacing.small + CoverVisualiser { + Layout.fillHeight: true + implicitWidth: Tokens.sizes.dashboard.mediaSectionWidth + } - PlayerControl { - type: IconButton.Text - icon: "move_up" - inactiveOnColour: Colours.palette.m3secondary - padding: Tokens.padding.small - font.pointSize: Tokens.font.size.large - disabled: !Players.active?.canRaise - onClicked: { - Players.active?.raise(); - root.visibilities.dashboard = false; + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + state: Players.active ? "" : "noMedia" + + states: State { + name: "noMedia" + + PropertyChanges { + noMedia.opacity: 1 + content.opacity: 0 + } } - } - SplitButton { - id: playerSelector + transitions: [ + Transition { + from: "" - disabled: !Players.list.length - active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null - menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData + SequentialAnimation { + Anim { + target: content + property: "opacity" + type: Anim.DefaultEffects + } + Anim { + target: noMedia + property: "opacity" + type: Anim.SlowEffects + } + } + }, + Transition { + to: "" - menuItems: playerList.instances - fallbackIcon: "music_off" - fallbackText: qsTr("No players") + SequentialAnimation { + Anim { + target: noMedia + property: "opacity" + type: Anim.DefaultEffects + } + Anim { + target: content + property: "opacity" + type: Anim.SlowEffects + } + } + } + ] - label.Layout.maximumWidth: slider.implicitWidth * 0.28 - label.elide: Text.ElideRight + Loader { + id: noMedia - stateLayer.disabled: true - menuOnTop: true + anchors.centerIn: parent + anchors.horizontalCenterOffset: -Tokens.padding.extraLarge * 2 + asynchronous: true + active: opacity > 0 + opacity: 0 - Variants { - id: playerList + sourceComponent: ColumnLayout { + spacing: Tokens.spacing.small - model: Players.list + MaterialShape { + Layout.topMargin: (pathBounds().height - implicitSize) / 2 + Layout.bottomMargin: (pathBounds().height - implicitSize) / 2 + Tokens.spacing.small + Layout.alignment: Qt.AlignHCenter + color: Colours.palette.m3primaryContainer + implicitSize: icon.implicitHeight + Tokens.padding.extraLarge * 2 + shape: MaterialShape.ClamShell - PlayerItem {} + Behavior on color { + CAnim {} + } + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: "queue_music" + fontStyle: Tokens.font.icon.builders.large.scale(2).build() + color: Colours.palette.m3onPrimaryContainer + } + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Nothing playing") + font: Tokens.font.headline.medium + } + + StyledText { + text: qsTr("Play something for it to show up here!") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.large + } + } } - } - PlayerControl { - type: IconButton.Text - icon: "delete" - inactiveOnColour: Colours.palette.m3error - padding: Tokens.padding.small - font.pointSize: Tokens.font.size.large - disabled: !Players.active?.canQuit - onClicked: Players.active?.quit() - } - } + Loader { + id: content - component PlayerItem: MenuItem { - required property MprisPlayer modelData + anchors.fill: parent + asynchronous: true + active: opacity > 0 - icon: modelData === Players.active ? "check" : "" - text: Players.getIdentity(modelData) - activeIcon: "animated_images" - } + sourceComponent: RowLayout { + spacing: Tokens.spacing.extraLarge - component PlayerControl: IconButton { - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) - radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : implicitHeight / 2 - radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial - radiusAnim.easing: Tokens.anim.expressiveFastSpatial + Details { + Layout.fillWidth: true + } - Behavior on Layout.preferredWidth { - Anim { - type: Anim.FastSpatial + LyricsAndSelector { + Layout.fillHeight: true + implicitWidth: Tokens.sizes.dashboard.mediaSectionWidth + } + } } } } diff --git a/modules/dashboard/MediaWrapper.qml b/modules/dashboard/MediaWrapper.qml deleted file mode 100644 index b03a11ee..00000000 --- a/modules/dashboard/MediaWrapper.qml +++ /dev/null @@ -1,13 +0,0 @@ -import QtQuick - -Item { - property alias visibilities: media.visibilities - readonly property alias needsKeyboard: media.needsKeyboard - - implicitWidth: media.implicitWidth - implicitHeight: media.nonAnimHeight - - Media { - id: media - } -} diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index c93499ca..3973bdc0 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -1,57 +1,47 @@ +import "performance" import QtQuick -import QtQuick.Controls import QtQuick.Layouts import Quickshell.Services.UPower import Caelestia.Config -import Caelestia.Internal +import Caelestia.Services import qs.components -import qs.components.misc import qs.services Item { id: root - readonly property int minWidth: 400 + 400 + Tokens.spacing.normal + 120 + Tokens.padding.large * 2 + implicitWidth: placeholder.active ? Tokens.sizes.dashboard.perfPlaceholderWidth : content.implicitWidth + implicitHeight: placeholder.active ? placeholder.implicitHeight + Tokens.padding.extraLarge * 2 : content.implicitHeight - function displayTemp(temp: real): string { - return `${Math.ceil(GlobalConfig.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${GlobalConfig.services.useFahrenheitPerformance ? "F" : "C"}`; - } - - implicitWidth: Math.max(minWidth, content.implicitWidth) - implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight - - StyledRect { + Loader { id: placeholder anchors.centerIn: parent - width: 400 - height: 350 - radius: Tokens.rounding.large - color: Colours.tPalette.m3surfaceContainer - visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery) + active: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None) && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery) + asynchronous: true - ColumnLayout { - anchors.centerIn: parent - spacing: Tokens.spacing.normal + sourceComponent: ColumnLayout { + spacing: Tokens.spacing.medium MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "tune" - font.pointSize: Tokens.font.size.extraLarge * 2 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).build() color: Colours.palette.m3onSurfaceVariant } StyledText { + Layout.topMargin: -Tokens.spacing.small Layout.alignment: Qt.AlignHCenter text: qsTr("No widgets enabled") - font.pointSize: Tokens.font.size.large + font: Tokens.font.title.large color: Colours.palette.m3onSurface } StyledText { Layout.alignment: Qt.AlignHCenter - text: qsTr("Enable widgets in dashboard settings") - font.pointSize: Tokens.font.size.small + text: qsTr("Enable widgets in the dashboard settings") + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } } @@ -62,765 +52,95 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Tokens.spacing.normal - visible: !placeholder.visible - - Ref { - service: SystemUsage - } + spacing: Tokens.spacing.medium + visible: !placeholder.active ColumnLayout { id: mainColumn Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") + spacing: Tokens.spacing.medium + visible: cpuCard.active || gpuCard.active - HeroCard { - Layout.fillWidth: true - Layout.minimumWidth: 400 - Layout.preferredHeight: 150 - visible: Config.dashboard.performance.showCpu - icon: "memory" - title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU") - mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%` - mainLabel: qsTr("Usage") - secondaryValue: root.displayTemp(SystemUsage.cpuTemp) - secondaryLabel: qsTr("Temp") - usage: SystemUsage.cpuPerc - temperature: SystemUsage.cpuTemp - accentColor: Colours.palette.m3primary - } + WrappedLoader { + id: cpuCard - HeroCard { - Layout.fillWidth: true - Layout.minimumWidth: 400 - Layout.preferredHeight: 150 - visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE" - icon: "desktop_windows" - title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU") - mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%` - mainLabel: qsTr("Usage") - secondaryValue: root.displayTemp(SystemUsage.gpuTemp) - secondaryLabel: qsTr("Temp") - usage: SystemUsage.gpuPerc - temperature: SystemUsage.gpuTemp - accentColor: Colours.palette.m3secondary - } - } + active: Config.dashboard.performance.showCpu - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork + sourceComponent: HeroCard { + icon: "memory" + label: qsTr("CPU") + subLabel: Cpu.name + usage: Cpu.percentage + temperature: Cpu.temperature + accent: Colours.palette.m3primary - GaugeCard { - Layout.minimumWidth: 250 - Layout.preferredHeight: 220 - Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork - icon: "memory_alt" - title: qsTr("Memory") - percentage: SystemUsage.memPerc - subtitle: { - const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed); - const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal); - return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; - } - accentColor: Colours.palette.m3tertiary - visible: Config.dashboard.performance.showMemory - } - - StorageGaugeCard { - Layout.minimumWidth: 250 - Layout.preferredHeight: 220 - Layout.fillWidth: !Config.dashboard.performance.showNetwork - visible: Config.dashboard.performance.showStorage - } - - NetworkCard { - Layout.fillWidth: true - Layout.minimumWidth: 200 - Layout.preferredHeight: 220 - visible: Config.dashboard.performance.showNetwork - } - } - } - - BatteryTank { - Layout.preferredWidth: 120 - Layout.preferredHeight: mainColumn.implicitHeight - visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery - } - } - - component BatteryTank: StyledClippingRect { - id: batteryTank - - property real percentage: UPower.displayDevice.percentage - property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging - property color accentColor: Colours.palette.m3primary - property real animatedPercentage: 0 - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.large - Component.onCompleted: animatedPercentage = percentage - onPercentageChanged: animatedPercentage = percentage - - // Background Fill - StyledRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: parent.height * batteryTank.animatedPercentage - color: Qt.alpha(batteryTank.accentColor, 0.15) - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.small - - // Header Section - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small - - MaterialIcon { - text: { - if (!UPower.displayDevice.isLaptopBattery) { - if (PowerProfiles.profile === PowerProfile.PowerSaver) - return "energy_savings_leaf"; - - if (PowerProfiles.profile === PowerProfile.Performance) - return "rocket_launch"; - - return "balance"; + ServiceRef { + service: Cpu } - if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) - return "battery_full"; - - const perc = UPower.displayDevice.percentage; - const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state); - if (perc >= 0.99) - return "battery_full"; - - let level = Math.floor(perc * 7); - if (charging && (level === 4 || level === 1)) - level--; - - return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; } - font.pointSize: Tokens.font.size.large - color: batteryTank.accentColor } - StyledText { - Layout.fillWidth: true - text: qsTr("Battery") - font.pointSize: Tokens.font.size.normal - color: Colours.palette.m3onSurface + WrappedLoader { + id: gpuCard + + active: Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None + + sourceComponent: HeroCard { + icon: "desktop_windows" + label: qsTr("GPU") + subLabel: Gpu.name + usage: Gpu.percentage + temperature: Gpu.temperature + accent: Colours.palette.m3secondary + + ServiceRef { + service: Gpu + } + } } } - Item { - Layout.fillHeight: true - } + RowLayout { + spacing: Tokens.spacing.medium + visible: storageCard.active || networkCard.active || memoryCard.active - // Bottom Info Section - ColumnLayout { - Layout.fillWidth: true - spacing: -4 + WrappedLoader { + id: storageCard - StyledText { - Layout.alignment: Qt.AlignRight - text: `${Math.round(batteryTank.percentage * 100)}%` - font.pointSize: Tokens.font.size.extraLarge - font.weight: Font.Medium - color: batteryTank.accentColor + active: Config.dashboard.performance.showStorage + sourceComponent: StorageCard {} } - StyledText { - Layout.alignment: Qt.AlignRight - text: { - if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) - return qsTr("Full"); + WrappedLoader { + id: networkCard - if (batteryTank.isCharging) - return qsTr("Charging"); + active: Config.dashboard.performance.showNetwork + sourceComponent: NetworkCard {} + } - const s = UPower.displayDevice.timeToEmpty; - if (s === 0) - return qsTr("..."); + WrappedLoader { + id: memoryCard - const hr = Math.floor(s / 3600); - const min = Math.floor((s % 3600) / 60); - if (hr > 0) - return `${hr}h ${min}m`; - - return `${min}m`; - } - font.pointSize: Tokens.font.size.smaller - color: Colours.palette.m3onSurfaceVariant + active: Config.dashboard.performance.showMemory + sourceComponent: MemoryCard {} } } } - Behavior on animatedPercentage { - Anim { - type: Anim.StandardLarge - } + WrappedLoader { + Layout.fillWidth: false + active: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery + sourceComponent: BatteryTank {} } } - component CardHeader: RowLayout { - property string icon - property string title - property color accentColor: Colours.palette.m3primary - + component WrappedLoader: Loader { Layout.fillWidth: true - spacing: Tokens.spacing.small - - MaterialIcon { - text: parent.icon - fill: 1 - color: parent.accentColor - font.pointSize: Tokens.spacing.large - } - - StyledText { - Layout.fillWidth: true - text: parent.title - font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - } - - component ProgressBar: StyledRect { - id: progressBar - - property real value: 0 - property color fgColor: Colours.palette.m3primary - property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - property real animatedValue: 0 - - color: bgColor - radius: Tokens.rounding.full - Component.onCompleted: animatedValue = value - onValueChanged: animatedValue = value - - StyledRect { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - width: parent.width * progressBar.animatedValue - color: progressBar.fgColor - radius: Tokens.rounding.full - } - - Behavior on animatedValue { - Anim { - type: Anim.StandardLarge - } - } - } - - component HeroCard: StyledClippingRect { - id: heroCard - - property string icon - property string title - property string mainValue - property string mainLabel - property string secondaryValue - property string secondaryLabel - property real usage: 0 - property real temperature: 0 - property color accentColor: Colours.palette.m3primary - readonly property real maxTemp: 100 - readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp)) - property real animatedUsage: 0 - property real animatedTemp: 0 - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.large - Component.onCompleted: { - animatedUsage = usage; - animatedTemp = tempProgress; - } - onUsageChanged: animatedUsage = usage - onTempProgressChanged: animatedTemp = tempProgress - - StyledRect { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - implicitWidth: parent.width * heroCard.animatedUsage - color: Qt.alpha(heroCard.accentColor, 0.15) - } - - CardHeader { - anchors.left: parent.left - anchors.top: parent.top - anchors.leftMargin: Tokens.padding.large - anchors.topMargin: Math.round(Tokens.padding.large * 1.2) - - width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Tokens.spacing.normal - icon: heroCard.icon - title: heroCard.title - accentColor: heroCard.accentColor - } - - Column { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Math.round(Tokens.padding.large * 1.2) - anchors.bottomMargin: Math.round(Tokens.padding.large * 1.3) - - spacing: Tokens.spacing.small - - Row { - spacing: Tokens.spacing.small - - StyledText { - text: heroCard.secondaryValue - font.pointSize: Tokens.font.size.normal - font.weight: Font.Medium - } - - StyledText { - text: heroCard.secondaryLabel - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - anchors.baseline: parent.children[0].baseline - } - } - - ProgressBar { - implicitWidth: parent.width * 0.5 - implicitHeight: 6 - value: heroCard.tempProgress - fgColor: heroCard.accentColor - bgColor: Qt.alpha(heroCard.accentColor, 0.2) - } - } - - Column { - id: usageColumn - - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - anchors.rightMargin: 32 - spacing: 0 - - StyledText { - id: usageLabel - - anchors.right: parent.right - text: heroCard.mainLabel - font.pointSize: Tokens.font.size.normal - color: Colours.palette.m3onSurfaceVariant - } - - StyledText { - anchors.right: parent.right - text: heroCard.mainValue - font.pointSize: Tokens.font.size.extraLarge - font.weight: Font.Medium - color: heroCard.accentColor - } - } - - Behavior on animatedUsage { - Anim { - type: Anim.StandardLarge - } - } - - Behavior on animatedTemp { - Anim { - type: Anim.StandardLarge - } - } - } - - component GaugeCard: StyledRect { - id: gaugeCard - - property string icon - property string title - property real percentage: 0 - property string subtitle - property color accentColor: Colours.palette.m3primary - readonly property real arcStartAngle: 0.75 * Math.PI - readonly property real arcSweep: 1.5 * Math.PI - property real animatedPercentage: 0 - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.large - clip: true - Component.onCompleted: animatedPercentage = percentage - onPercentageChanged: animatedPercentage = percentage - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.smaller - - CardHeader { - icon: gaugeCard.icon - title: gaugeCard.title - accentColor: gaugeCard.accentColor - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ArcGauge { - anchors.centerIn: parent - width: Math.min(parent.width, parent.height) - height: width - percentage: gaugeCard.animatedPercentage - accentColor: gaugeCard.accentColor - trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - startAngle: gaugeCard.arcStartAngle - sweepAngle: gaugeCard.arcSweep - } - - StyledText { - anchors.centerIn: parent - text: `${Math.round(gaugeCard.percentage * 100)}%` - font.pointSize: Tokens.font.size.extraLarge - font.weight: Font.Medium - color: gaugeCard.accentColor - } - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: gaugeCard.subtitle - font.pointSize: Tokens.font.size.smaller - color: Colours.palette.m3onSurfaceVariant - } - } - - Behavior on animatedPercentage { - Anim { - type: Anim.StandardLarge - } - } - } - - component StorageGaugeCard: StyledRect { - id: storageGaugeCard - - property int currentDiskIndex: 0 - readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null - property int diskCount: 0 - readonly property real arcStartAngle: 0.75 * Math.PI - readonly property real arcSweep: 1.5 * Math.PI - property real animatedPercentage: 0 - property color accentColor: Colours.palette.m3secondary - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.large - clip: true - Component.onCompleted: { - diskCount = SystemUsage.disks.length; - if (currentDisk) - animatedPercentage = currentDisk.perc; - } - onCurrentDiskChanged: { - if (currentDisk) - animatedPercentage = currentDisk.perc; - } - - // Update diskCount and animatedPercentage when disks data changes - Connections { - function onDisksChanged() { - if (SystemUsage.disks.length !== storageGaugeCard.diskCount) - storageGaugeCard.diskCount = SystemUsage.disks.length; - - // Update animated percentage when disk data refreshes - if (storageGaugeCard.currentDisk) - storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc; - } - - target: SystemUsage - } - - MouseArea { - anchors.fill: parent - onWheel: wheel => { - if (wheel.angleDelta.y > 0) - storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount; - else if (wheel.angleDelta.y < 0) - storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount; - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.smaller - - CardHeader { - icon: "hard_disk" - title: { - const base = qsTr("Storage"); - if (!storageGaugeCard.currentDisk) - return base; - - return `${base} - ${storageGaugeCard.currentDisk.mount}`; - } - accentColor: storageGaugeCard.accentColor - - // Scroll hint icon - MaterialIcon { - text: "unfold_more" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.normal - visible: storageGaugeCard.diskCount > 1 - opacity: 0.7 - ToolTip.visible: hintHover.hovered - ToolTip.text: qsTr("Scroll to switch disks") - ToolTip.delay: 500 - - HoverHandler { - id: hintHover - } - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ArcGauge { - anchors.centerIn: parent - width: Math.min(parent.width, parent.height) - height: width - percentage: storageGaugeCard.animatedPercentage - accentColor: storageGaugeCard.accentColor - trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - startAngle: storageGaugeCard.arcStartAngle - sweepAngle: storageGaugeCard.arcSweep - } - - StyledText { - anchors.centerIn: parent - text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—" - font.pointSize: Tokens.font.size.extraLarge - font.weight: Font.Medium - color: storageGaugeCard.accentColor - } - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: { - if (!storageGaugeCard.currentDisk) - return "—"; - - const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used); - const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total); - return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; - } - font.pointSize: Tokens.font.size.smaller - color: Colours.palette.m3onSurfaceVariant - } - } - - Behavior on animatedPercentage { - Anim { - type: Anim.StandardLarge - } - } - } - - component NetworkCard: StyledRect { - id: networkCard - - property color accentColor: Colours.palette.m3primary - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.large - clip: true - - Ref { - service: NetworkUsage - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.small - - CardHeader { - icon: "swap_vert" - title: qsTr("Network") - accentColor: networkCard.accentColor - } - - // Sparkline graph - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - SparklineItem { - id: sparkline - - property real targetMax: 1024 - property real smoothMax: targetMax - - anchors.fill: parent - line1: NetworkUsage.uploadBuffer // qmllint disable missing-type - line1Color: Colours.palette.m3secondary - line1FillAlpha: 0.15 - line2: NetworkUsage.downloadBuffer // qmllint disable missing-type - line2Color: Colours.palette.m3tertiary - line2FillAlpha: 0.2 - maxValue: smoothMax - historyLength: NetworkUsage.historyLength - - Connections { - function onValuesChanged(): void { - sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); - slideAnim.restart(); - } - - target: NetworkUsage.downloadBuffer - } - - NumberAnimation { - id: slideAnim - - target: sparkline - property: "slideProgress" - from: 0 - to: 1 - duration: GlobalConfig.dashboard.resourceUpdateInterval - } - - Behavior on smoothMax { - Anim { - type: Anim.StandardLarge - } - } - } - - // "No data" placeholder - StyledText { - anchors.centerIn: parent - text: qsTr("Collecting data...") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - visible: NetworkUsage.downloadBuffer.count < 2 - opacity: 0.6 - } - } - - // Download row - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - MaterialIcon { - text: "download" - color: Colours.palette.m3tertiary - font.pointSize: Tokens.font.size.normal - } - - StyledText { - text: qsTr("Download") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - Item { - Layout.fillWidth: true - } - - StyledText { - text: { - const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); - return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; - } - font.pointSize: Tokens.font.size.normal - font.weight: Font.Medium - color: Colours.palette.m3tertiary - } - } - - // Upload row - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - MaterialIcon { - text: "upload" - color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.normal - } - - StyledText { - text: qsTr("Upload") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - Item { - Layout.fillWidth: true - } - - StyledText { - text: { - const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); - return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; - } - font.pointSize: Tokens.font.size.normal - font.weight: Font.Medium - color: Colours.palette.m3secondary - } - } - - // Session totals - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - MaterialIcon { - text: "history" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.normal - } - - StyledText { - text: qsTr("Total") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - Item { - Layout.fillWidth: true - } - - StyledText { - text: { - const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0); - const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0); - return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B"; - } - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - } - } + Layout.fillHeight: true + visible: active } } diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 5bf99ca3..e67cfd5e 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Templates import Quickshell -import Quickshell.Widgets import Caelestia.Config import qs.components import qs.components.controls @@ -18,7 +18,7 @@ Item { readonly property alias count: bar.count - implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight + implicitHeight: bar.implicitHeight + bar.anchors.topMargin + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight TabBar { id: bar @@ -26,12 +26,21 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top + anchors.topMargin: Tokens.sizes.dashboard.tabIndicatorSpacing currentIndex: root.dashState.currentTab - background: null - onCurrentIndexChanged: root.dashState.currentTab = currentIndex + implicitHeight: contentHeight + background: null + contentItem: RowLayout { + spacing: 0 + + Repeater { + model: bar.contentModel + } + } + Repeater { model: ScriptModel { values: root.tabs @@ -106,6 +115,10 @@ Item { required property string iconName readonly property bool current: TabBar.tabBar.currentItem === this + Layout.fillWidth: true + Layout.preferredWidth: 1 // Uniform width across all tabs + implicitWidth: implicitContentWidth + implicitHeight: implicitContentHeight background: null contentItem: CustomMouseArea { @@ -124,92 +137,20 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor - onPressed: event => { - root.dashState.currentTab = tab.TabBar.index; + onPressed: root.dashState.currentTab = tab.TabBar.index - const stateY = stateWrapper.y; - rippleAnim.x = event.x; - rippleAnim.y = event.y - stateY; - - const dist = (ox, oy) => ox * ox + oy * oy; - rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y + stateY), dist(event.x, stateWrapper.height - event.y), dist(width - event.x, event.y + stateY), dist(width - event.x, stateWrapper.height - event.y))); - - rippleAnim.restart(); - } - - SequentialAnimation { - id: rippleAnim - - property real x - property real y - property real radius - - PropertyAction { - target: ripple - property: "x" - value: rippleAnim.x - } - PropertyAction { - target: ripple - property: "y" - value: rippleAnim.y - } - PropertyAction { - target: ripple - property: "opacity" - value: 0.08 - } - Anim { - target: ripple - properties: "implicitWidth,implicitHeight" - from: 0 - to: rippleAnim.radius * 2 - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standardDecel - } - Anim { - target: ripple - property: "opacity" - to: 0 - } - } - - ClippingRectangle { - id: stateWrapper + StateLayer { + id: stateLayer + anchors.fill: undefined anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter implicitHeight: parent.height + Tokens.sizes.dashboard.tabIndicatorSpacing * 2 - color: "transparent" - radius: Tokens.rounding.small - - StyledRect { - id: stateLayer - - anchors.fill: parent - - color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface - opacity: mouse.pressed ? 0.1 : mouse.containsMouse ? 0.08 : 0 - - Behavior on opacity { - Anim {} - } - } - - StyledRect { - id: ripple - - radius: Tokens.rounding.full - color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface - opacity: 0 - - transform: Translate { - x: -ripple.width / 2 - y: -ripple.height / 2 - } - } + radius: Tokens.rounding.medium + color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface + onClicked: root.dashState.currentTab = tab.TabBar.index } MaterialIcon { @@ -221,10 +162,12 @@ Item { text: tab.iconName color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant fill: tab.current ? 1 : 0 - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.medium Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/modules/dashboard/WeatherTab.qml b/modules/dashboard/WeatherTab.qml index 3cf8d3b7..b942a8b0 100644 --- a/modules/dashboard/WeatherTab.qml +++ b/modules/dashboard/WeatherTab.qml @@ -17,7 +17,7 @@ Item { id: layout anchors.fill: parent - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium RowLayout { Layout.leftMargin: Tokens.padding.large @@ -25,18 +25,17 @@ Item { Layout.fillWidth: true Column { - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { text: Weather.city || qsTr("Loading...") - font.pointSize: Tokens.font.size.extraLarge - font.weight: 600 + font: Tokens.font.body.builders.large.size(28).weight(Font.DemiBold).build() color: Colours.palette.m3onSurface } StyledText { text: new Date().toLocaleDateString(Qt.locale(), "dddd, MMMM d") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } } @@ -46,7 +45,7 @@ Item { } Row { - spacing: Tokens.spacing.large + spacing: Tokens.spacing.largeIncreased WeatherStat { icon: "wb_twilight" @@ -66,21 +65,21 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small * 2 + implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small - radius: Tokens.rounding.large * 2 + radius: Tokens.rounding.extraLarge * 2 color: Colours.tPalette.m3surfaceContainer RowLayout { id: bigInfoRow anchors.centerIn: parent - spacing: Tokens.spacing.large + spacing: Tokens.spacing.largeIncreased MaterialIcon { Layout.alignment: Qt.AlignVCenter text: Weather.icon - font.pointSize: Tokens.font.size.extraLarge * 3 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).build() color: Colours.palette.m3secondary animate: true } @@ -91,15 +90,14 @@ Item { StyledText { text: Weather.temp - font.pointSize: Tokens.font.size.extraLarge * 2 - font.weight: 500 + font: Tokens.font.body.builders.large.size(28 * 2).weight(Font.Medium).build() color: Colours.palette.m3primary } StyledText { - Layout.leftMargin: Tokens.padding.small + Layout.leftMargin: Tokens.padding.extraSmall text: Weather.description - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium color: Colours.palette.m3onSurfaceVariant } } @@ -108,7 +106,7 @@ Item { RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium DetailCard { icon: "water_drop" @@ -131,18 +129,17 @@ Item { } StyledText { - Layout.topMargin: Tokens.spacing.normal - Layout.leftMargin: Tokens.padding.normal + Layout.topMargin: Tokens.spacing.medium + Layout.leftMargin: Tokens.padding.medium visible: forecastRepeater.count > 0 text: qsTr("7-Day Forecast") - font.pointSize: Tokens.font.size.normal - font.weight: 600 + font: Tokens.font.body.builders.medium.weight(Font.DemiBold).build() color: Colours.palette.m3onSurface } RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium Repeater { id: forecastRepeater @@ -156,9 +153,9 @@ Item { required property var modelData Layout.fillWidth: true - implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.medium * 2 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -170,16 +167,15 @@ Item { StyledText { Layout.alignment: Qt.AlignHCenter text: forecastItem.index === 0 ? qsTr("Today") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "ddd") - font.pointSize: Tokens.font.size.normal - font.weight: 600 + font: Tokens.font.body.builders.medium.weight(Font.DemiBold).build() color: Colours.palette.m3primary } StyledText { - Layout.topMargin: -Tokens.spacing.small / 2 + Layout.topMargin: -Tokens.spacing.extraSmall Layout.alignment: Qt.AlignHCenter text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "MMM d") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small opacity: 0.7 color: Colours.palette.m3onSurfaceVariant } @@ -187,14 +183,14 @@ Item { MaterialIcon { Layout.alignment: Qt.AlignHCenter text: forecastItem.modelData.icon - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge color: Colours.palette.m3secondary } StyledText { Layout.alignment: Qt.AlignHCenter text: GlobalConfig.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" - font.weight: 600 + font: Tokens.font.body.builders.small.weight(Font.DemiBold).build() color: Colours.palette.m3tertiary } } @@ -213,17 +209,17 @@ Item { Layout.fillWidth: true Layout.preferredHeight: 60 - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.tPalette.m3surfaceContainer Row { anchors.centerIn: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { text: detailRoot.icon color: detailRoot.colour - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large anchors.verticalCenter: parent.verticalCenter } @@ -233,13 +229,13 @@ Item { StyledText { text: detailRoot.label - font.pointSize: Tokens.font.size.smaller + font: Tokens.font.body.small opacity: 0.7 horizontalAlignment: Text.AlignLeft } StyledText { text: detailRoot.value - font.weight: 600 + font: Tokens.font.body.builders.small.weight(Font.DemiBold).build() horizontalAlignment: Text.AlignLeft } } @@ -258,20 +254,19 @@ Item { MaterialIcon { text: weatherStat.icon - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge color: weatherStat.colour } Column { StyledText { text: weatherStat.label - font.pointSize: Tokens.font.size.smaller + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } StyledText { text: weatherStat.value - font.pointSize: Tokens.font.size.small - font.weight: 600 + font: Tokens.font.body.builders.small.weight(Font.DemiBold).build() color: Colours.palette.m3onSurface } } diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index f7f03742..5debd7ad 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -12,7 +12,6 @@ Item { id: root required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: (content.item as Content)?.needsKeyboard ?? false readonly property DashboardState dashState: DashboardState { reloadableId: "dashboardState" } @@ -28,7 +27,7 @@ Item { } } - readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 + readonly property real nonAnimHeight: (content.item as Content)?.nonAnimHeight ?? 0 readonly property bool shouldBeActive: visibilities.dashboard && Config.dashboard.enabled property real offsetScale: shouldBeActive ? 0 : 1 @@ -39,9 +38,7 @@ Item { opacity: 1 - offsetScale Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index e2af3c01..9061f2ee 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import QtQuick.Layouts +import M3Shapes import Caelestia.Config import qs.components import qs.components.controls @@ -36,57 +37,43 @@ CustomMouseArea { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.small + spacing: Tokens.spacing.extraSmall RowLayout { id: monthNavigationRow Layout.fillWidth: true - spacing: Tokens.spacing.small + spacing: Tokens.spacing.extraSmall - Item { - implicitWidth: implicitHeight - implicitHeight: prevMonthText.implicitHeight + Tokens.padding.small * 2 - - StateLayer { - id: prevMonthStateLayer - - radius: Tokens.rounding.full - onClicked: root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1) - } - - MaterialIcon { - id: prevMonthText - - anchors.centerIn: parent - text: "chevron_left" - color: Colours.palette.m3tertiary - font.pointSize: Tokens.font.size.normal - font.weight: 700 - } + IconButton { + icon: "chevron_left" + type: IconButton.Text + font: Tokens.font.icon.builders.small.weight(Font.Bold).build() + padding: Tokens.padding.small + onClicked: root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1) } Item { Layout.fillWidth: true + Layout.fillHeight: true - implicitWidth: monthYearDisplay.implicitWidth + Tokens.padding.small * 2 - implicitHeight: monthYearDisplay.implicitHeight + Tokens.padding.small * 2 + implicitWidth: monthYearDisplay.implicitWidth + Tokens.padding.large * 2 + implicitHeight: monthYearDisplay.implicitHeight + Tokens.padding.extraSmall * 2 StateLayer { - onClicked: { - root.dashState.currentDate = new Date(); - } - - anchors.fill: monthYearDisplay - anchors.margins: -Tokens.padding.small - anchors.leftMargin: -Tokens.padding.normal - anchors.rightMargin: -Tokens.padding.normal - - radius: Tokens.rounding.full + color: Colours.palette.m3primary + radius: pressed ? Tokens.rounding.small : Tokens.rounding.large disabled: { const now = new Date(); return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); } + onClicked: root.dashState.currentDate = new Date() + + Behavior on radius { + Anim { + type: Anim.DefaultEffects + } + } } StyledText { @@ -95,35 +82,16 @@ CustomMouseArea { anchors.centerIn: parent text: grid.title color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.normal - font.weight: 500 - font.capitalization: Font.Capitalize + font: Tokens.font.title.builders.small.capitalisation(Font.Capitalize).build() } } - Item { - implicitWidth: implicitHeight - implicitHeight: nextMonthText.implicitHeight + Tokens.padding.small * 2 - - StateLayer { - id: nextMonthStateLayer - - onClicked: { - root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); - } - - radius: Tokens.rounding.full - } - - MaterialIcon { - id: nextMonthText - - anchors.centerIn: parent - text: "chevron_right" - color: Colours.palette.m3tertiary - font.pointSize: Tokens.font.size.normal - font.weight: 700 - } + IconButton { + icon: "chevron_right" + type: IconButton.Text + font: Tokens.font.icon.builders.small.weight(Font.Bold).build() + padding: Tokens.padding.small + onClicked: root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1) } } @@ -138,8 +106,8 @@ CustomMouseArea { horizontalAlignment: Text.AlignHCenter text: model.shortName - font.weight: 500 - color: (model.day === 0 || model.day === 6) ? Colours.palette.m3secondary : Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.builders.small.weight(Font.Medium).build() + color: (model.day === 0 || model.day === 6) ? Colours.palette.m3tertiary : Colours.palette.m3onSurface } } @@ -164,7 +132,7 @@ CustomMouseArea { required property var model implicitWidth: implicitHeight - implicitHeight: text.implicitHeight + Tokens.padding.small * 2 + implicitHeight: text.implicitHeight + Tokens.padding.small StyledText { id: text @@ -174,20 +142,19 @@ CustomMouseArea { horizontalAlignment: Text.AlignHCenter text: grid.locale.toString(dayItem.model.day) color: { - const dayOfWeek = dayItem.model.date.getUTCDay(); + const dayOfWeek = dayItem.model.date.getDay(); if (dayOfWeek === 0 || dayOfWeek === 6) - return Colours.palette.m3secondary; + return Colours.palette.m3tertiary; return Colours.palette.m3onSurfaceVariant; } opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4 - font.pointSize: Tokens.font.size.normal - font.weight: 500 + font: Tokens.font.body.small } } } - StyledRect { + MaterialShape { id: todayIndicator readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null @@ -199,13 +166,12 @@ CustomMouseArea { } x: today ? today.x + (today.width - implicitWidth) / 2 : 0 - y: today?.y ?? 0 + y: today ? today.y - Tokens.padding.extraSmall - 1 : 0 - implicitWidth: today?.implicitWidth ?? 0 - implicitHeight: today?.implicitHeight ?? 0 + implicitSize: today ? Math.max(today.implicitWidth, today.implicitHeight) + Tokens.padding.extraSmall * 2 : 0 + shape: MaterialShape.Sunny clip: true - radius: Tokens.rounding.full color: Colours.palette.m3primary opacity: todayItem ? 1 : 0 @@ -224,23 +190,23 @@ CustomMouseArea { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { - Anim {} + Anim { + type: Anim.FastSpatial + } } Behavior on x { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on y { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index 82bee151..5b7f4bb7 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -24,17 +24,14 @@ Item { Layout.alignment: Qt.AlignHCenter text: Time.hourStr color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.extraLarge - font.family: Tokens.font.family.clock - font.weight: 600 + font: Tokens.font.clock.size(28).weight(Font.DemiBold).build() } StyledText { Layout.alignment: Qt.AlignHCenter text: "•••" color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.extraLarge * 0.9 - font.family: Tokens.font.family.clock + font: Tokens.font.clock.size(28 * 0.9).build() } StyledText { @@ -42,9 +39,7 @@ Item { Layout.alignment: Qt.AlignHCenter text: Time.minuteStr color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.extraLarge - font.family: Tokens.font.family.clock - font.weight: 600 + font: Tokens.font.clock.size(28).weight(Font.DemiBold).build() } Loader { @@ -57,9 +52,7 @@ Item { sourceComponent: StyledText { text: Time.amPmStr color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.large - font.family: Tokens.font.family.clock - font.weight: 600 + font: Tokens.font.clock.size(18).weight(Font.DemiBold).build() } } } diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index a14e5e38..bbd8a351 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,9 +1,12 @@ +pragma ComponentBehavior: Bound + import QtQuick -import QtQuick.Shapes -import Quickshell +import Caelestia.Components import Caelestia.Config import Caelestia.Services import qs.components +import qs.components.controls +import qs.components.widgets import qs.services import qs.utils @@ -15,6 +18,8 @@ Item { return active?.length ? (active.position % active.length) / active.length : 0; } + readonly property real arcCoverGap: Tokens.spacing.extraSmall + anchors.top: parent.top anchors.bottom: parent.bottom implicitWidth: Tokens.sizes.dashboard.mediaWidth @@ -37,84 +42,32 @@ Item { service: Audio.beatTracker } - Shape { - preferredRendererType: Shape.CurveRenderer + CircularProgress { + id: prog - ShapePath { - fillColor: "transparent" - strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + anchors.centerIn: cover + implicitSize: cover.width + root.arcCoverGap + thickness * 2 - PathAngleArc { - centerX: cover.x + cover.width / 2 - centerY: cover.y + cover.height / 2 - radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small - radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small - startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 - sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep - } + fgColour: Colours.palette.m3primary + strokeWidth: Tokens.sizes.dashboard.mediaProgressThickness + startAngle: -90 - sweepAngle / 2 + sweepAngle: Tokens.sizes.dashboard.mediaProgressSweep + value: root.playerProgress - Behavior on strokeColor { - CAnim {} - } - } - - ShapePath { - fillColor: "transparent" - strokeColor: Colours.palette.m3primary - strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - - PathAngleArc { - centerX: cover.x + cover.width / 2 - centerY: cover.y + cover.height / 2 - radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small - radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small - startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 - sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep * root.playerProgress - } - - Behavior on strokeColor { - CAnim {} - } - } + wavy: true + waveFrequency: 8 + waveDuration: 2000 + wavePaused: !Players.active?.isPlaying } - StyledClippingRect { + CoverArt { id: cover anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Tokens.padding.large + Tokens.sizes.dashboard.mediaProgressThickness + Tokens.spacing.small - + anchors.margins: Tokens.padding.medium + root.arcCoverGap + prog.thickness implicitHeight: width - color: Colours.tPalette.m3surfaceContainerHigh - radius: Infinity - - MaterialIcon { - anchors.centerIn: parent - - grade: 200 - text: "art_track" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: (parent.width * 0.4) || 1 - } - - Image { - id: image - - anchors.fill: parent - - source: Players.getArtUrl(Players.active) - asynchronous: true - fillMode: Image.PreserveAspectCrop - sourceSize: { - const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; - return Qt.size(width * dpr, height * dpr); - } - } } StyledText { @@ -122,15 +75,15 @@ Item { anchors.top: cover.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Tokens.spacing.normal + anchors.topMargin: Tokens.spacing.medium animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small - width: parent.implicitWidth - Tokens.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.extraLargeIncreased elide: Text.ElideRight } @@ -145,9 +98,9 @@ Item { horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small - width: parent.implicitWidth - Tokens.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.extraLargeIncreased elide: Text.ElideRight } @@ -163,34 +116,46 @@ Item { text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") color: Colours.palette.m3secondary - width: parent.implicitWidth - Tokens.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.extraLargeIncreased elide: Text.ElideRight } - Row { + ButtonRow { id: controls anchors.top: artist.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Tokens.spacing.smaller + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: Tokens.spacing.medium + anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.small + spacing: Tokens.spacing.extraSmall - PlayerControl { + IconButton { + type: IconButton.Tonal icon: "skip_previous" - canUse: Players.active?.canGoPrevious ?? false + isRound: true + shapeMorph: true + disabled: !Players.active?.canGoPrevious onClicked: Players.active?.previous() } - PlayerControl { + IconButton { + fillWidth: true icon: Players.active?.isPlaying ? "pause" : "play_arrow" - canUse: Players.active?.canTogglePlaying ?? false + isRound: true + shapeMorph: true + checked: Players.active?.isPlaying ?? false + disabled: !Players.active?.canTogglePlaying onClicked: Players.active?.togglePlaying() } - PlayerControl { + IconButton { + type: IconButton.Tonal icon: "skip_next" - canUse: Players.active?.canGoNext ?? false + isRound: true + shapeMorph: true + disabled: !Players.active?.canGoNext onClicked: Players.active?.next() } } @@ -204,7 +169,7 @@ Item { anchors.right: parent.right anchors.topMargin: Tokens.spacing.small anchors.bottomMargin: Tokens.padding.large - anchors.margins: Tokens.padding.large * 2 + anchors.margins: Tokens.padding.extraLargeIncreased playing: Players.active?.isPlaying ?? false speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type @@ -212,34 +177,4 @@ Item { asynchronous: true fillMode: AnimatedImage.PreserveAspectFit } - - component PlayerControl: StyledRect { - id: control - - required property string icon - required property bool canUse - - signal clicked - - implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Tokens.padding.small - implicitHeight: implicitWidth - - StateLayer { - disabled: !control.canUse - radius: Tokens.rounding.full - onClicked: control.clicked() - } - - MaterialIcon { - id: icon - - anchors.centerIn: parent - anchors.verticalCenterOffset: font.pointSize * 0.05 - - animate: true - text: control.icon - color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline - font.pointSize: Tokens.font.size.large - } - } } diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index 2e0f085b..898f69a4 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -1,87 +1,75 @@ import QtQuick +import QtQuick.Layouts import Caelestia.Config +import Caelestia.Services import qs.components -import qs.components.misc +import qs.components.controls import qs.services -Row { +Item { id: root anchors.top: parent.top anchors.bottom: parent.bottom - padding: Tokens.padding.large - spacing: Tokens.spacing.normal + implicitWidth: layout.implicitWidth + layout.anchors.margins * 2 - Ref { - service: SystemUsage + ServiceRef { + service: Cpu } - Resource { - icon: "memory" - value: SystemUsage.cpuPerc - colour: Colours.palette.m3primary + ServiceRef { + service: Memory } - Resource { - icon: "memory_alt" - value: SystemUsage.memPerc - colour: Colours.palette.m3secondary + ServiceRef { + service: Storage } - Resource { - icon: "hard_disk" - value: SystemUsage.storagePerc - colour: Colours.palette.m3tertiary - } - - component Resource: Item { - id: res - - required property string icon - required property real value - required property color colour + ColumnLayout { + id: layout + anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: Tokens.padding.large - implicitWidth: icon.implicitWidth + spacing: Tokens.spacing.medium - StyledRect { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.bottom: icon.top - anchors.bottomMargin: Tokens.spacing.small + Resource { + icon: "memory" + value: Cpu.percentage + } - implicitWidth: Tokens.sizes.dashboard.resourceProgressThickness + Resource { + icon: "memory_alt" + value: Memory.percentage + fgColour: Colours.palette.m3tertiary + } - color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Tokens.rounding.full + Resource { + icon: "hard_disk" + value: Storage.percentage + fgColour: Colours.palette.m3secondary + } + } + component Resource: CircularProgress { + id: res - StyledRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - implicitHeight: res.value * parent.height + required property string icon - color: res.colour - radius: Tokens.rounding.full - } + Layout.fillHeight: true + implicitSize: height + strokeWidth: Tokens.sizes.dashboard.resourceProgressThickness + + Behavior on clampedVal { + Anim {} } MaterialIcon { - id: icon - - anchors.bottom: parent.bottom - + anchors.centerIn: parent text: res.icon - color: res.colour - } - - Behavior on value { - Anim { - type: Anim.StandardLarge - } + font: Tokens.font.icon.large + color: res.fgColour } } } diff --git a/modules/dashboard/dash/SmallWeather.qml b/modules/dashboard/dash/SmallWeather.qml index 998c7125..cd5f3e40 100644 --- a/modules/dashboard/dash/SmallWeather.qml +++ b/modules/dashboard/dash/SmallWeather.qml @@ -9,6 +9,7 @@ Item { anchors.centerIn: parent implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin + implicitHeight: Math.max(icon.implicitHeight, info.implicitHeight) + Tokens.padding.largeIncreased * 2 Component.onCompleted: Weather.reload() @@ -21,7 +22,7 @@ Item { animate: true text: Weather.icon color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.extraLarge * 2 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(1.6).build() } Column { @@ -29,9 +30,9 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: icon.right - anchors.leftMargin: Tokens.spacing.large + anchors.leftMargin: Tokens.spacing.largeIncreased - spacing: Tokens.spacing.small + spacing: Tokens.spacing.extraSmall StyledText { anchors.horizontalCenter: parent.horizontalCenter @@ -39,8 +40,7 @@ Item { animate: true text: Weather.temp color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 + font: Tokens.font.headline.builders.medium.width(110).weight(Font.DemiBold).build() } StyledText { @@ -48,9 +48,10 @@ Item { animate: true text: Weather.description + font: Tokens.font.body.small elide: Text.ElideRight - width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Tokens.padding.large * 2) + width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Tokens.padding.extraLargeIncreased) } } } diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 79787de5..d8d56128 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -1,4 +1,7 @@ +pragma ComponentBehavior: Bound + import QtQuick +import M3Shapes import Caelestia.Config import qs.components import qs.components.effects @@ -7,186 +10,267 @@ import qs.components.images import qs.services import qs.utils -Row { +Item { id: root required property DrawerVisibilities visibilities required property FileDialog facePicker - padding: Tokens.padding.large - spacing: Tokens.spacing.normal + property color pfpFallbackColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) - StyledClippingRect { - implicitWidth: info.implicitHeight - implicitHeight: info.implicitHeight + anchors.fill: parent + anchors.margins: Tokens.padding.large - radius: Tokens.rounding.large - color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + Behavior on pfpFallbackColour { + CAnim {} + } + + Item { + id: pfpContainer + + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: logoShape.right + anchors.leftMargin: -(Tokens.padding.largeIncreased + Tokens.padding.extraLarge) / 2 + implicitWidth: height + + MaterialShape { + id: shape - MaterialIcon { anchors.centerIn: parent + implicitSize: parent.height + shape: MaterialShape.Pill + color: Qt.alpha(root.pfpFallbackColour, 1) + opacity: root.pfpFallbackColour.a + layer.enabled: true - text: "person" - fill: 1 - grade: 200 - font.pointSize: Math.floor(info.implicitHeight / 2) || 1 - visible: pfp.status !== Image.Ready + MouseArea { + id: mouse + + containmentMask: QtObject { + function contains(pt: point): bool { + return shape.contains(pt) && !logoShape.contains(mouse.mapToItem(logoShape, pt)) && !uptimeShape.contains(mouse.mapToItem(uptimeShape, pt)); + } + } + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.visibilities.dashboard = false; + root.facePicker.open(); + } + } } - CachingImage { - id: pfp - + Item { anchors.fill: parent - path: `${Paths.home}/.face` - } + layer.enabled: true + layer.effect: Mask { + maskSource: shape + } - MouseArea { - anchors.fill: parent - hoverEnabled: true + Loader { + anchors.centerIn: parent + asynchronous: true + active: pfp.status !== Image.Ready + + sourceComponent: MaterialIcon { + text: "person_add" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.extraLarge + fill: 1 + grade: -2 // Ugh material symbols are such a pain with fill + } + } + + CachingImage { + id: pfp + + anchors.fill: parent + path: `${Paths.home}/.face` + } StyledRect { anchors.fill: parent - - color: Qt.alpha(Colours.palette.m3scrim, 0.5) - opacity: parent.containsMouse ? 1 : 0 + color: Qt.alpha(Colours.palette.m3scrim, pfp.status === Image.Ready ? 0.4 : 0) + opacity: mouse.containsMouse ? 1 : 0 + layer.enabled: opacity < 1 Behavior on opacity { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - } - } - } - - StyledRect { - anchors.centerIn: parent - - implicitWidth: selectIcon.implicitHeight + Tokens.padding.small * 2 - implicitHeight: selectIcon.implicitHeight + Tokens.padding.small * 2 - - radius: Tokens.rounding.normal - color: Colours.palette.m3primary - scale: parent.containsMouse ? 1 : 0.5 - opacity: parent.containsMouse ? 1 : 0 - - StateLayer { - color: Colours.palette.m3onPrimary - onClicked: { - root.visibilities.launcher = false; - root.facePicker.open(); + type: Anim.DefaultEffects } } - MaterialIcon { - id: selectIcon - + MaterialShape { anchors.centerIn: parent - anchors.horizontalCenterOffset: -font.pointSize * 0.02 + implicitSize: parent.height * 0.7 + shape: MaterialShape.Diamond + color: Colours.palette.m3primary + scale: mouse.pressed ? 0.9 : mouse.containsMouse ? 1 : 0.7 - text: "frame_person" - color: Colours.palette.m3onPrimary - font.pointSize: Tokens.font.size.extraLarge - } - - Behavior on scale { - Anim { - type: Anim.FastSpatial + Behavior on color { + CAnim {} } - } - Behavior on opacity { - Anim { - duration: Tokens.anim.durations.expressiveFastSpatial + Behavior on scale { + Anim { + type: Anim.FastSpatial + } + } + + MaterialIcon { + anchors.centerIn: parent + text: "person_edit" + color: Colours.palette.m3onPrimary + fontStyle: Tokens.font.icon.large } } } } } - Column { - id: info + MaterialShape { + id: logoShape - anchors.verticalCenter: parent.verticalCenter - spacing: Tokens.spacing.normal + x: Tokens.padding.extraSmall + implicitSize: Tokens.sizes.dashboard.logoSize + Tokens.padding.small * 2 + shape: MaterialShape.Gem + color: Colours.palette.m3primaryContainer - Item { - id: line + Behavior on color { + CAnim {} + } - implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin - implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight) + Loader { + anchors.centerIn: parent + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : osLogo + } + } - ColouredIcon { - id: icon + Component { + id: osLogo - anchors.left: parent.left - anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 + ColouredIcon { + id: icon - source: SysInfo.osLogo - implicitSize: Math.floor(Tokens.font.size.normal * 1.34) - colour: Colours.palette.m3primary + source: SysInfo.osLogo + implicitSize: Tokens.sizes.dashboard.logoSize + colour: Colours.palette.m3onPrimaryContainer + } + } + + Component { + id: caelestiaLogo + + Logo { + implicitWidth: Tokens.sizes.dashboard.logoSize + implicitHeight: Tokens.sizes.dashboard.logoSize + topColour: Colours.palette.m3primary + bottomColour: Colours.palette.m3onPrimaryContainer + } + } + + MaterialShape { + id: uptimeShape + + anchors.bottom: parent.bottom + anchors.left: pfpContainer.right + anchors.bottomMargin: -Tokens.padding.small // Clamshell is taller than what it is visually + anchors.leftMargin: -Tokens.padding.extraLargeIncreased + implicitSize: Tokens.sizes.dashboard.uptimeSize + Tokens.padding.small * 2 + shape: MaterialShape.ClamShell + color: Colours.palette.m3tertiaryContainer + + Behavior on color { + CAnim {} + } + + MaterialIcon { + anchors.centerIn: parent + text: "clock_arrow_up" + color: Colours.palette.m3onTertiaryContainer + fontStyle: Tokens.font.icon.medium + } + } + + StyledText { + anchors.left: uptimeShape.right + anchors.verticalCenter: uptimeShape.verticalCenter + anchors.leftMargin: Tokens.spacing.small + anchors.verticalCenterOffset: Math.round(fontInfo.pointSize * 0.1) + + text: "up " + SysInfo.uptime.split(",").slice(0, 2).join(",") // Max 2 components + width: Tokens.sizes.dashboard.userWidth - x - Tokens.padding.extraLarge + elide: Text.ElideRight + } + + StyledRect { + id: bubble1 + + anchors.left: pfpContainer.right + anchors.top: bubble2.bottom + anchors.leftMargin: Tokens.spacing.small + anchors.topMargin: -Tokens.spacing.extraSmall + + implicitWidth: 10 + implicitHeight: 10 + radius: Tokens.rounding.full + color: Colours.palette.m3secondaryContainer + } + + StyledRect { + id: bubble2 + + anchors.left: bubble1.right + anchors.verticalCenter: wmContainer.bottom + anchors.leftMargin: Tokens.spacing.extraSmall + + implicitWidth: 15 + implicitHeight: 15 + radius: Tokens.rounding.full + color: Colours.palette.m3secondaryContainer + } + + StyledRect { + id: wmContainer + + anchors.left: bubble2.left + anchors.leftMargin: -Tokens.padding.medium + y: Tokens.padding.extraSmall + + radius: Tokens.rounding.largeIncreased + color: Colours.palette.m3secondaryContainer + implicitWidth: wmLabel.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: wmLabel.implicitHeight + Tokens.padding.small * 2 + + Row { + id: wmLabel + + anchors.centerIn: parent + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + id: wmIcon + + anchors.verticalCenter: parent.verticalCenter + text: "select_window" + color: Colours.palette.m3onSecondaryContainer + fontStyle: wmText.font } StyledText { - id: text + id: wmText - anchors.verticalCenter: icon.verticalCenter - anchors.left: icon.right - anchors.leftMargin: icon.anchors.leftMargin - text: `: ${SysInfo.osPrettyName || SysInfo.osName}` - font.pointSize: Tokens.font.size.normal - - width: Tokens.sizes.dashboard.infoWidth + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: Math.round(fontInfo.pointSize * 0.1) + text: SysInfo.wm + "..." + color: Colours.palette.m3onSecondaryContainer + font: Tokens.font.body.builders.small.vaxis("slnt", -4).build() + width: Math.min(implicitWidth, Tokens.sizes.dashboard.userWidth - wmContainer.x - Tokens.padding.medium * 2 - wmIcon.implicitWidth - wmLabel.spacing - Tokens.padding.extraLarge) elide: Text.ElideRight } } - - InfoLine { - icon: "select_window_2" - text: SysInfo.wm - colour: Colours.palette.m3secondary - } - - InfoLine { - id: uptime - - icon: "timer" - text: qsTr("up %1").arg(SysInfo.uptime) - colour: Colours.palette.m3tertiary - } - } - - component InfoLine: Item { - id: line - - required property string icon - required property string text - required property color colour - - implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin - implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight) - - MaterialIcon { - id: icon - - anchors.left: parent.left - anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 - - fill: 1 - text: line.icon - color: line.colour - font.pointSize: Tokens.font.size.normal - } - - StyledText { - id: text - - anchors.verticalCenter: icon.verticalCenter - anchors.left: icon.right - anchors.leftMargin: icon.anchors.leftMargin - text: `: ${line.text}` - font.pointSize: Tokens.font.size.normal - - width: Tokens.sizes.dashboard.infoWidth - elide: Text.ElideRight - } } } diff --git a/modules/dashboard/media/BackgroundShapes.qml b/modules/dashboard/media/BackgroundShapes.qml new file mode 100644 index 00000000..8523b9b1 --- /dev/null +++ b/modules/dashboard/media/BackgroundShapes.qml @@ -0,0 +1,97 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import M3Shapes +import qs.components +import qs.services + +Item { + id: root + + readonly property list shapePool: [MaterialShape.Circle, MaterialShape.Cookie4Sided, MaterialShape.Cookie6Sided, MaterialShape.Cookie7Sided, MaterialShape.Cookie9Sided, MaterialShape.Cookie12Sided, MaterialShape.Sunny, MaterialShape.VerySunny, MaterialShape.SoftBurst, MaterialShape.Pentagon, MaterialShape.Gem, MaterialShape.Arch, MaterialShape.Arrow, MaterialShape.Pill, MaterialShape.Triangle, MaterialShape.Fan, MaterialShape.Oval] + + property int count: 14 + property real minSize: 36 + property real maxSize: 124 + property real minSpeed: 4 + property real maxSpeed: 18 + property real minRotSpeed: -12 + property real maxRotSpeed: 12 + property list lightOpacities: [0.34, 0.34, 0.08, 0.2] + property list darkOpacities: [0.16, 0.16, 0.04, 0.16] + + function rand(min: real, max: real): real { + return min + Math.random() * (max - min); + } + + function signedRand(min: real, max: real): real { + return rand(min, max) * (Math.random() < 0.5 ? -1 : 1); + } + + clip: true + Component.onCompleted: shapes.model = count + + Repeater { + id: shapes + + DriftingShape {} + } + + FrameAnimation { + running: root.visible && root.width > 0 && root.height > 0 && (Players.active?.isPlaying ?? false) + onTriggered: { + const dt = frameTime; + for (let i = 0; i < shapes.count; i++) { + const s = shapes.itemAt(i) as DriftingShape; + if (!s) + continue; + + s.x += s.vx * dt; + s.y += s.vy * dt; + s.rotation += s.vr * dt; + + if (s.x + s.width < 0) + s.x = root.width; + else if (s.x > root.width) + s.x = -s.width; + + if (s.y + s.height < 0) + s.y = root.height; + else if (s.y > root.height) + s.y = -s.height; + } + } + } + + component DriftingShape: MaterialShape { + id: shapeItem + + required property int index + + property real vx: root.signedRand(root.minSpeed, root.maxSpeed) + property real vy: root.signedRand(root.minSpeed, root.maxSpeed) + property real vr: root.rand(root.minRotSpeed, root.maxRotSpeed) + readonly property int colourIdx: Math.floor(Math.random() * 4) + + implicitSize: root.minSize + (index / root.count) * (root.maxSize - root.minSize) + shape: root.shapePool[Math.floor(Math.random() * root.shapePool.length)] + color: [Colours.palette.m3primaryContainer, Colours.palette.m3secondaryContainer, Colours.palette.m3tertiaryContainer, Colours.palette.m3outlineVariant][colourIdx] + opacity: Colours.light ? root.lightOpacities[colourIdx] : root.darkOpacities[colourIdx] + rotation: root.rand(0, 360) + + Component.onCompleted: { + x = root.rand(0, root.width - implicitSize); + y = root.rand(0, root.height - implicitSize); + } + + Behavior on color { + CAnim {} + } + + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } + } +} diff --git a/modules/dashboard/media/CoverVisualiser.qml b/modules/dashboard/media/CoverVisualiser.qml new file mode 100644 index 00000000..e642b466 --- /dev/null +++ b/modules/dashboard/media/CoverVisualiser.qml @@ -0,0 +1,82 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Shapes +import Quickshell +import M3Shapes +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.components.widgets +import qs.services + +Item { + id: root + + readonly property real centerX: width / 2 + readonly property real centerY: height / 2 + readonly property real spacing: Tokens.spacing.medium + readonly property real maxMagnitude: (implicitWidth - cover.implicitWidth) / 2 - spacing + + ServiceRef { + service: Audio.cava + } + + Shape { + anchors.fill: parent + asynchronous: true + preferredRendererType: Shape.CurveRenderer + data: bars.instances + } + + Variants { + id: bars + + model: Array.from({ + length: GlobalConfig.services.visualiserBars + }, (_, i) => i) + + ShapePath { + id: bar + + required property int modelData + readonly property real value: Math.max(1e-2, Math.min(1, Audio.cava.values[modelData])) + + readonly property real angle: modelData * 2 * Math.PI / GlobalConfig.services.visualiserBars + readonly property real dist: shapeEdgeDist + value * root.maxMagnitude + readonly property real shapeEdgeDist: { + cover.shape.rotation; // Update when shape rotation changes + const sDist = cover.shape.distanceAtAngle(modelData * 360 / GlobalConfig.services.visualiserBars + 90); + return sDist + root.spacing + strokeWidth / 2; + } + readonly property real cos: Math.cos(angle) + readonly property real sin: Math.sin(angle) + + asynchronous: true + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + strokeWidth: 360 / GlobalConfig.services.visualiserBars - root.Tokens.spacing.small / 4 + strokeColor: Colours.palette.m3primary + + startX: root.centerX + shapeEdgeDist * cos + startY: root.centerY + shapeEdgeDist * sin + + PathLine { + x: root.centerX + bar.dist * bar.cos + y: root.centerY + bar.dist * bar.sin + } + + Behavior on strokeColor { + CAnim {} + } + } + } + + CoverArt { + id: cover + + anchors.centerIn: parent + shape.shape: MaterialShape.Cookie9Sided + implicitWidth: Tokens.sizes.dashboard.mediaCoverArtSize + implicitHeight: Tokens.sizes.dashboard.mediaCoverArtSize + } +} diff --git a/modules/dashboard/media/Details.qml b/modules/dashboard/media/Details.qml new file mode 100644 index 00000000..2e15a7b0 --- /dev/null +++ b/modules/dashboard/media/Details.qml @@ -0,0 +1,180 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.Mpris +import Caelestia.Components +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +ColumnLayout { + id: root + + function lengthStr(length: int): string { + if (length < 0) + return "-1:-1"; + + const hours = Math.floor(length / 3600); + const mins = Math.floor((length % 3600) / 60); + const secs = Math.floor(length % 60).toString().padStart(2, "0"); + + if (hours > 0) + return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`; + return `${mins}:${secs}`; + } + + spacing: Tokens.spacing.extraSmall + + Timer { + running: Players.active?.isPlaying ?? false + interval: GlobalConfig.dashboard.mediaUpdateInterval + triggeredOnStart: true + repeat: true + onTriggered: Players.active?.positionChanged() + } + + StyledText { + Layout.fillWidth: true + text: Players.active?.trackTitle ?? "" + font: Tokens.font.title.large + elide: Text.ElideRight + animate: true + } + + StyledText { + Layout.fillWidth: true + text: Players.active?.trackArtist || qsTr("Unknown artist") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.title.medium + elide: Text.ElideRight + animate: true + } + + StyledText { + Layout.fillWidth: true + text: Players.active?.trackAlbum || qsTr("Unknown album") + color: Colours.palette.m3secondary + font: Tokens.font.title.medium + elide: Text.ElideRight + animate: true + } + + RowLayout { + Layout.topMargin: Tokens.spacing.extraLargeIncreased + Layout.fillWidth: true + spacing: Tokens.spacing.small + + TextMetrics { + id: timeMetrics + + text: Players.active ? root.lengthStr(Math.max(Players.active.position, Players.active.length)).replace(/[1-9]/g, "0") : "00:00" + font: Tokens.font.label.medium + } + + StyledText { + Layout.preferredWidth: timeMetrics.width + text: root.lengthStr(Players.active?.position ?? -1) + color: Colours.palette.m3onSurfaceVariant + font: timeMetrics.font + horizontalAlignment: Text.AlignHCenter + } + + StyledSlider { + Layout.fillWidth: true + value: Players.active ? Players.active.position / (Players.active.length || 1) : 0 + enabled: Players.active?.canSeek ?? false + wavy: true + animateWave: Players.active?.isPlaying ?? false + waveFrequency: 5 + waveDuration: 2000 + onInteraction: value => { + const active = Players.active; + if (active?.canSeek && active?.positionSupported) + active.position = value * active.length; + } + } + + StyledText { + Layout.preferredWidth: timeMetrics.width + text: root.lengthStr(Players.active?.length ?? -1) + color: Colours.palette.m3onSurfaceVariant + font: timeMetrics.font + horizontalAlignment: Text.AlignHCenter + } + } + + ButtonRow { + Layout.topMargin: Tokens.spacing.largeIncreased + Layout.fillWidth: true + spacing: Tokens.spacing.extraSmall + + IconButton { + type: IconButton.Tonal + icon: "shuffle" + isRound: true + shapeMorph: true + checked: Players.active?.shuffle ?? false + font: Tokens.font.icon.builders.medium.weight(Font.Medium).build() + disabled: !Players.active?.shuffleSupported + onClicked: Players.active.shuffle = !Players.active?.shuffle + implicitWidth: Math.round(implicitHeight * 0.9) + } + + IconButton { + id: previousBtn + + type: IconButton.Tonal + icon: "skip_previous" + isRound: true + shapeMorph: true + font: Tokens.font.icon.large + disabled: !Players.active?.canGoPrevious + onClicked: Players.active?.previous() + } + + IconButton { + id: playPauseBtn + + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + isRound: true + shapeMorph: true + fillWidth: true + checked: Players.active?.isPlaying ?? false + font: Tokens.font.icon.large + disabled: !Players.active?.canTogglePlaying + onClicked: Players.active?.togglePlaying() + } + + IconButton { + id: nextBtn + + type: IconButton.Tonal + icon: "skip_next" + isRound: true + shapeMorph: true + font: Tokens.font.icon.large + disabled: !Players.active?.canGoNext + onClicked: Players.active?.next() + } + + IconButton { + type: IconButton.Tonal + icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat" + isRound: true + shapeMorph: true + checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist + font: Tokens.font.icon.builders.medium.weight(Font.Medium).build() + disabled: !Players.active?.loopSupported + onClicked: { + const state = Players.active.loopState; + if (state === MprisLoopState.None) + Players.active.loopState = MprisLoopState.Track; + else if (state === MprisLoopState.Track) + Players.active.loopState = MprisLoopState.Playlist; + else + Players.active.loopState = MprisLoopState.None; + } + implicitWidth: Math.round(implicitHeight * 0.9) + } + } +} diff --git a/modules/dashboard/media/LyricList.qml b/modules/dashboard/media/LyricList.qml new file mode 100644 index 00000000..0d44293c --- /dev/null +++ b/modules/dashboard/media/LyricList.qml @@ -0,0 +1,313 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.components.containers +import qs.components.controls +import qs.components.effects +import qs.services + +Item { + id: root + + // Funny binding hack to make lyrics update + readonly property var _: { + const p = Players.active; + if (p) + Lyrics.setTrack(p.trackArtist, p.trackTitle, p.trackAlbum, p.length); + else + Lyrics.clearTrack(); + } + + readonly property real fadeAmount: 0.1 + property bool flag + property list lyricList: Lyrics.lyrics + + layer.enabled: true + layer.effect: Mask { + maskSource: mask + + Rectangle { + id: mask + + layer.enabled: true + visible: false + implicitWidth: root.width + implicitHeight: root.height + + gradient: Gradient { + orientation: Gradient.Vertical + + GradientStop { + color: Qt.alpha("black", 0) + position: 0 + } + GradientStop { + color: Qt.alpha("black", 1) + position: root.fadeAmount + } + GradientStop { + color: Qt.alpha("black", 1) + position: 1 - root.fadeAmount + } + GradientStop { + color: Qt.alpha("black", 0) + position: 1 + } + } + } + } + + state: { + flag; // For some reason it doesn't update sometimes, so use this to force an update + if (Lyrics.hasLyrics) + return "hasLyrics"; + if (Lyrics.loading) + return "loading"; + return "noLyrics"; + } + + states: [ + State { + name: "loading" + + PropertyChanges { + loadingIndicator.opacity: 1 + lyrics.opacity: 0 + noLyrics.opacity: 0 + } + }, + State { + name: "hasLyrics" + + PropertyChanges { + loadingIndicator.opacity: 0 + lyrics.opacity: 1 + noLyrics.opacity: 0 + } + }, + State { + name: "noLyrics" + + PropertyChanges { + loadingIndicator.opacity: 0 + lyrics.opacity: 0 + noLyrics.opacity: 1 + } + } + ] + + transitions: [ + Transition { + from: "loading" + + SequentialAnimation { + Anim { + target: loadingIndicator + property: "opacity" + type: Anim.DefaultEffects + } + Anim { + targets: [lyrics, noLyrics] + property: "opacity" + type: Anim.SlowEffects + } + } + }, + Transition { + from: "hasLyrics" + + SequentialAnimation { + Anim { + target: lyrics + property: "opacity" + type: Anim.DefaultEffects + } + Anim { + targets: [loadingIndicator, noLyrics] + property: "opacity" + type: Anim.SlowEffects + } + } + }, + Transition { + from: "noLyrics" + + SequentialAnimation { + Anim { + target: noLyrics + property: "opacity" + type: Anim.DefaultEffects + } + Anim { + targets: [loadingIndicator, lyrics] + property: "opacity" + type: Anim.SlowEffects + } + } + } + ] + + Connections { + function onHasLyricsChanged() { + root.flag = !root.flag; + } + + target: Lyrics + } + + Loader { + id: loadingIndicator + + anchors.centerIn: parent + asynchronous: true + active: opacity > 0 + opacity: 0 + + sourceComponent: ColumnLayout { + spacing: Tokens.spacing.large + + StyledRect { + Layout.alignment: Qt.AlignHCenter + implicitWidth: shape.implicitSize + Tokens.padding.medium * 2 + implicitHeight: shape.implicitSize + Tokens.padding.medium * 2 + color: Colours.palette.m3primaryContainer + radius: Tokens.rounding.full + + LoadingIndicator { + id: shape + + anchors.centerIn: parent + implicitSize: Math.round(Tokens.sizes.dashboard.mediaSectionWidth / 5) + containsIcon: true // This removes the pentagon, which is not centered + } + } + + StyledText { + text: qsTr("Loading lyrics...") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.title.medium + } + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Loader { + id: noLyrics + + anchors.centerIn: parent + asynchronous: true + active: opacity > 0 + opacity: 0 + + sourceComponent: ColumnLayout { + spacing: Tokens.spacing.small + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "sentiment_sad" + fontStyle: Tokens.font.icon.builders.large.scale(2).build() + color: Colours.palette.m3outline + } + + StyledText { + text: qsTr("No lyrics found") + color: Colours.palette.m3outline + font: Tokens.font.title.medium + } + } + } + + StyledListView { + id: lyrics + + anchors.fill: parent + anchors.topMargin: parent.height * root.fadeAmount / 2 + anchors.bottomMargin: parent.height * root.fadeAmount / 2 + + displayMarginBeginning: anchors.topMargin + displayMarginEnd: anchors.bottomMargin + + model: root.lyricList + Component.onCompleted: { + currentIndex = Qt.binding(() => { + model; // Force update when lyrics change + return Lyrics.indexForTime(Players.active?.position ?? 0); + }); + positionViewAtIndex(currentIndex, ListView.Center); + } + onModelChanged: Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)) + + highlightRangeMode: ListView.ApplyRange + highlightMoveDuration: Tokens.anim.durations.large + highlightMoveVelocity: -1 + preferredHighlightBegin: (height - (currentItem?.implicitHeight ?? 0)) / 2 + preferredHighlightEnd: (height + (currentItem?.implicitHeight ?? 0)) / 2 + + spacing: Tokens.spacing.small + opacity: 0 + + delegate: StyledText { + id: lyric + + required property string modelData + property real effectScale: ListView.isCurrentItem ? 1 : 0 + + anchors.left: ListView.view?.contentItem.left + anchors.right: ListView.view?.contentItem.right + + text: modelData || ". . ." + color: ListView.isCurrentItem ? Colours.palette.m3primary : Colours.palette.m3outline + font: Tokens.font.body.medium + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + + layer.enabled: effectScale > 0 + layer.effect: MultiEffect { + shadowEnabled: true + shadowColor: Colours.palette.m3primary + shadowOpacity: 0.5 * lyric.effectScale + shadowBlur: 0.6 * lyric.effectScale + blur: 0.4 * lyric.effectScale + } + + Behavior on effectScale { + Anim { + type: Anim.SlowEffects + } + } + } + + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } + } + + Behavior on lyricList { + SequentialAnimation { + Anim { + target: lyrics + property: "opacity" + to: 0 + type: Anim.DefaultEffects + } + PropertyAction {} + Anim { + target: lyrics + property: "opacity" + to: 1 + type: Anim.SlowEffects + } + } + } +} diff --git a/modules/dashboard/media/LyricsAndSelector.qml b/modules/dashboard/media/LyricsAndSelector.qml new file mode 100644 index 00000000..89d7ce5f --- /dev/null +++ b/modules/dashboard/media/LyricsAndSelector.qml @@ -0,0 +1,79 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Mpris +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +Item { + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.leftMargin: Tokens.padding.medium + spacing: Tokens.spacing.medium + + RowLayout { + Layout.bottomMargin: -Tokens.spacing.medium + spacing: Tokens.spacing.medium + z: 1 + + MaterialIcon { + Layout.topMargin: Math.round(fontInfo.pointSize * 0.12) + text: "lyrics" + fontStyle: Tokens.font.icon.medium + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Lyrics") + font: Tokens.font.title.medium + } + + LyricsInfo {} + } + + LyricList { + Layout.fillWidth: true + Layout.fillHeight: true + } + + SplitButton { + Layout.alignment: Qt.AlignHCenter + + type: SplitButton.Tonal + disabled: !Players.list.length + active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null + menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData + + menuItems: playerList.instances + fallbackIcon: "music_off" + fallbackText: qsTr("No players") + + minLeftWidth: layout.width - expandBtn.implicitWidth - spacing + label.Layout.maximumWidth: minLeftWidth - iconLabel.implicitWidth - textRow.spacing - textRow.anchors.horizontalCenterOffset / 2 - horizontalPadding * 2 + label.elide: Text.ElideRight + + stateLayer.disabled: true + menuOnTop: true + + Variants { + id: playerList + + model: Players.list + + PlayerItem {} + } + } + } + + component PlayerItem: MenuItem { + required property MprisPlayer modelData + + icon: modelData === Players.active ? "check" : "" + text: Players.getIdentity(modelData) + activeIcon: "animated_images" + } +} diff --git a/modules/dashboard/media/LyricsInfo.qml b/modules/dashboard/media/LyricsInfo.qml new file mode 100644 index 00000000..4091f05a --- /dev/null +++ b/modules/dashboard/media/LyricsInfo.qml @@ -0,0 +1,208 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Blobs +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.services + +Item { + id: root + + property bool open + readonly property real padding: Tokens.padding.large + + implicitWidth: btn.implicitWidth * 0.9 + implicitHeight: btn.implicitHeight * 0.9 + + BlobGroup { + id: blobGroup + + color: Colours.palette.m3surfaceContainerHighest + smoothing: root.Tokens.rounding.medium + cornerFill: false + + Behavior on color { + CAnim {} + } + } + + BlobRect { + id: btnRect + + anchors.fill: parent + anchors.margins: !btn.pressed && btn.containsMouse ? -Tokens.padding.extraSmall : 0 + group: blobGroup + radius: Tokens.rounding.medium + + Behavior on anchors.margins { + Anim {} + } + } + + BlobRect { + id: rect + + anchors.right: parent.right + anchors.top: parent.top + + implicitWidth: parent.width + implicitHeight: parent.height + + group: blobGroup + radius: Tokens.rounding.medium + deformScale: 0.00001 + + states: State { + name: "open" + when: root.open + + PropertyChanges { + rect.anchors.rightMargin: root.width - root.Tokens.spacing.small + rect.anchors.topMargin: -root.Tokens.padding.medium + rect.implicitWidth: Math.max(layout.implicitWidth, placeholder.implicitWidth) + root.padding * 2 + rect.implicitHeight: Math.max(layout.implicitHeight, placeholder.implicitHeight) + root.padding * 2 + content.opacity: 1 + } + } + + transitions: Transition { + Anim { + properties: "rightMargin,implicitWidth" + } + Anim { + properties: "topMargin,implicitHeight" + easing: root.Tokens.anim.expressiveFastSpatial + } + Anim { + property: "opacity" + type: Anim.DefaultEffects + } + } + + Behavior on implicitWidth { + Anim {} + } + + Behavior on implicitHeight { + Anim {} + } + + Item { + id: content + + anchors.fill: parent + clip: true + opacity: 0 + state: Lyrics.loading || !Lyrics.hasLyrics ? "" : "hasLyrics" + + states: State { + name: "hasLyrics" + + PropertyChanges { + layout.opacity: 1 + placeholder.opacity: 0 + } + } + + transitions: [ + Transition { + from: "hasLyrics" + + SequentialAnimation { + Anim { + target: layout + property: "opacity" + type: Anim.FastEffects + } + Anim { + target: placeholder + property: "opacity" + type: Anim.DefaultEffects + } + } + }, + Transition { + to: "hasLyrics" + + SequentialAnimation { + Anim { + target: placeholder + property: "opacity" + type: Anim.FastEffects + } + Anim { + target: layout + property: "opacity" + type: Anim.DefaultEffects + } + } + } + ] + + ColumnLayout { + id: layout + + anchors.centerIn: parent + spacing: Tokens.spacing.extraSmall + opacity: 0 + + StyledText { + text: qsTr("Backend: %1").arg(LyricsBackend.toString(Lyrics.backend)) + color: Colours.palette.m3onSurfaceVariant + animate: true + } + + StyledText { + Layout.maximumWidth: Tokens.sizes.dashboard.mediaTabWidth / 2 + text: qsTr("Selected candidate: %1 | %2 | %3").arg(Lyrics.selectedCandidate.title).arg(Lyrics.selectedCandidate.artist).arg(Lyrics.selectedCandidate.album) + color: Colours.palette.m3onSurfaceVariant + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + animate: true + } + + StyledText { + text: qsTr("Offset: %1 ms").arg(Lyrics.offset) + color: Colours.palette.m3onSurfaceVariant + animate: true + } + } + + Item { + id: placeholder + + anchors.centerIn: parent + implicitWidth: placeholderText.implicitWidth + implicitHeight: placeholderText.implicitHeight + + StyledText { + id: placeholderText + + text: Lyrics.loading ? qsTr("Loading...") : qsTr("No lyrics found") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.medium + animate: true + } + } + } + } + + MouseArea { + id: btn + + anchors.centerIn: parent + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Tokens.padding.extraSmall * 2 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: root.open = !root.open + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: "more_vert" + fontStyle: Tokens.font.icon.medium + } + } +} diff --git a/modules/dashboard/performance/BatteryTank.qml b/modules/dashboard/performance/BatteryTank.qml new file mode 100644 index 00000000..691405db --- /dev/null +++ b/modules/dashboard/performance/BatteryTank.qml @@ -0,0 +1,147 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.UPower +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.services + +StyledClippingRect { + id: root + + property real animPerc: UPower.displayDevice.percentage + + color: Colours.palette.m3secondaryContainer + radius: Tokens.rounding.large + + implicitWidth: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None) || Config.dashboard.performance.showStorage || Config.dashboard.performance.showMemory ? Tokens.sizes.dashboard.perfBattWidth : Tokens.sizes.dashboard.perfBattWidthSingle + implicitHeight: Tokens.sizes.dashboard.perfBattHeight + + Behavior on animPerc { + Anim {} + } + + Contents { + id: layout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + + accentColour: Colours.palette.m3primary + textColour: Colours.palette.m3onSurface + subTextColour: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + implicitHeight: parent.height * root.animPerc + + color: Colours.palette.m3secondary + radius: Tokens.rounding.extraSmall + clip: true + + Contents { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: layout.anchors.margins + height: layout.height + + accentColour: Colours.palette.m3primaryContainer + textColour: Colours.palette.m3onSecondary + subTextColour: Colours.palette.m3secondaryContainer + } + } + + component Contents: ColumnLayout { + id: contents + + required property color accentColour + required property color textColour + required property color subTextColour + readonly property bool charging: [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state) + + spacing: 0 + + MaterialIcon { + Layout.leftMargin: -Tokens.padding.extraSmall + text: "battery_full" + color: contents.accentColour + fontStyle: Tokens.font.icon.large + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Battery") + color: contents.textColour + font: Tokens.font.body.medium + } + + Item { + Layout.fillHeight: true + } + + StyledText { + Layout.alignment: Qt.AlignRight + text: { + if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) + return qsTr("Full"); + + if (contents.charging) + return qsTr("Charging"); + + const s = UPower.displayDevice.timeToEmpty; + if (s === 0) + return qsTr("..."); + + const hr = Math.floor(s / 3600); + const min = Math.floor((s % 3600) / 60); + if (hr > 0) + return `${hr}h ${min}m`; + + return `${min}m`; + } + color: contents.subTextColour + font: Tokens.font.body.small + animate: true + } + + RowLayout { + Layout.topMargin: -Tokens.padding.extraSmall + Layout.bottomMargin: -Tokens.padding.small + Layout.rightMargin: -Tokens.padding.extraSmall + Layout.alignment: Qt.AlignRight + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + text: "bolt" + color: contents.accentColour + fontStyle: Tokens.font.icon.large + fill: 1 + + scale: contents.charging ? 1 : 0 + opacity: contents.charging ? 1 : 0 + + Behavior on scale { + Anim { + type: Anim.FastSpatial + } + } + + Behavior on opacity { + Anim { + type: Anim.FastEffects + } + } + } + + StyledText { + text: `${Math.round(UPower.displayDevice.percentage * 100)}%` + color: contents.accentColour + font: Tokens.font.headline.medium + } + } + } +} diff --git a/modules/dashboard/performance/HeroCard.qml b/modules/dashboard/performance/HeroCard.qml new file mode 100644 index 00000000..866c6c49 --- /dev/null +++ b/modules/dashboard/performance/HeroCard.qml @@ -0,0 +1,148 @@ +import QtQuick +import QtQuick.Layouts +import M3Shapes +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +StyledRect { + id: root + + required property string icon + required property string label + required property string subLabel + required property color accent + required property real usage + required property real temperature + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.extraLarge + + implicitWidth: Tokens.sizes.dashboard.perfHeroCardWidth + implicitHeight: Math.max(tempProg.implicitHeight + detailsRow.implicitHeight + Tokens.spacing.large, usageShape.implicitHeight + usageLabel.implicitHeight) + Tokens.padding.large * 2 + + CircularProgress { + id: tempProg + + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Tokens.padding.large + + fgColour: root.accent + + spacing: Tokens.spacing.extraSmall + strokeWidth: Tokens.padding.extraSmall + implicitSize: Math.max(icon.implicitWidth, icon.implicitHeight) + Tokens.padding.medium * 2 + value: root.usage + + Behavior on clampedVal { + Anim {} + } + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: root.icon + color: root.accent + fontStyle: Tokens.font.icon.medium + } + } + + ColumnLayout { + anchors.left: tempProg.right + anchors.right: parent.right + anchors.verticalCenter: tempProg.verticalCenter + anchors.margins: Tokens.spacing.large + spacing: Tokens.spacing.extraSmall + + StyledText { + text: root.label + font: Tokens.font.title.medium + color: root.accent + } + + StyledText { + Layout.fillWidth: true + text: root.subLabel + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + elide: Text.ElideRight + } + } + + ColumnLayout { + id: detailsRow + + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: Tokens.padding.largeIncreased + spacing: Tokens.spacing.extraSmall + + RowLayout { + Layout.leftMargin: -Tokens.padding.extraSmall + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + Layout.topMargin: Math.round(fontInfo.pointSize * 0.08) + text: root.temperature > 90 ? "thermometer_alert" : "thermometer" + color: root.temperature > 90 ? Colours.palette.m3error : root.accent + fontStyle: Tokens.font.icon.medium + fill: 1 + } + + StyledText { + text: `${Math.ceil(GlobalConfig.services.useFahrenheitPerformance ? root.temperature * 1.8 + 32 : root.temperature)}°${GlobalConfig.services.useFahrenheitPerformance ? "F" : "C"}` + font: Tokens.font.body.builders.medium.build() + } + } + + StyledProgressBar { + value: root.temperature / 100 + implicitHeight: Tokens.padding.small + fgColour: root.accent + indeterminate: isNaN(root.usage) || isNaN(root.temperature) + } + } + + MaterialShape { + id: usageShape + + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Tokens.padding.medium + + implicitSize: Tokens.sizes.dashboard.perfUsageShapeSize + color: Colours.palette.m3secondaryContainer + shape: { + if (root.usage >= 0.8) + return MaterialShape.SoftBurst; + if (root.usage >= 0.4) + return MaterialShape.Sunny; + return MaterialShape.Cookie4Sided; + } + + Behavior on color { + CAnim {} + } + + StyledText { + id: usageLabel + + anchors.bottom: parent.top + anchors.horizontalCenter: parent.horizontalCenter + + text: qsTr("Usage") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.small + } + + StyledText { + anchors.centerIn: parent + text: isNaN(root.usage) ? "...%" : Math.round(root.usage * 100) + "%" + color: root.accent + font: Tokens.font.headline.builders.small.width(50).build() + } + } +} diff --git a/modules/dashboard/performance/MemoryCard.qml b/modules/dashboard/performance/MemoryCard.qml new file mode 100644 index 00000000..5caf4c1e --- /dev/null +++ b/modules/dashboard/performance/MemoryCard.qml @@ -0,0 +1,93 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.components.controls +import qs.services + +StyledRect { + id: root + + readonly property color accent: Colours.palette.m3tertiary + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.medium + + implicitWidth: layout.implicitWidth + Tokens.padding.extraLargeIncreased * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + + ServiceRef { + service: Memory + } + + ColumnLayout { + id: layout + + anchors.centerIn: parent + spacing: Tokens.spacing.extraSmall + + RowLayout { + Layout.leftMargin: -Tokens.padding.extraSmall + spacing: Tokens.spacing.small + + MaterialIcon { + text: "memory_alt" + fill: 1 + color: root.accent + fontStyle: Tokens.font.icon.builders.medium.weight(Font.DemiBold).build() // DemiBold to fix fill issues + } + + StyledText { + text: qsTr("Memory") + font: Tokens.font.title.medium + } + } + + CircularProgress { + Layout.topMargin: Tokens.spacing.large + Layout.alignment: Qt.AlignHCenter + implicitSize: usageColumn.implicitHeight + thickness + Tokens.padding.largeIncreased * 2 + startAngle: -225 + sweepAngle: 270 + + fgColour: root.accent + value: Memory.percentage + + Behavior on clampedVal { + Anim {} + } + + ColumnLayout { + id: usageColumn + + anchors.centerIn: parent + anchors.verticalCenterOffset: Tokens.padding.extraSmall + spacing: 0 + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Math.round(Memory.percentage * 100) + "%" + font: Tokens.font.title.builders.large.width(90).build() + color: root.accent + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Used") + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + } + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: { + const fmt = UsageFmt.formatKib(Memory.used, Memory.total); + return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`; + } + font: Tokens.font.body.medium + } + } +} diff --git a/modules/dashboard/performance/NetworkCard.qml b/modules/dashboard/performance/NetworkCard.qml new file mode 100644 index 00000000..4eccebe7 --- /dev/null +++ b/modules/dashboard/performance/NetworkCard.qml @@ -0,0 +1,197 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import Caelestia.Internal +import qs.components +import qs.components.misc +import qs.services + +StyledRect { + id: root + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.extraLarge + + implicitWidth: Tokens.sizes.dashboard.perfNetworkCardWidth + implicitHeight: Tokens.sizes.dashboard.perfNetworkCardHeight + + Ref { + service: NetworkUsage + } + + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.margins: Tokens.padding.large + anchors.bottomMargin: Tokens.padding.medium + spacing: 0 + + RowLayout { + spacing: Tokens.spacing.small + + MaterialIcon { + text: "swap_vert" + color: Colours.palette.m3primary + fontStyle: Tokens.font.icon.medium + } + + StyledText { + text: qsTr("Network") + font: Tokens.font.title.medium + } + } + + // Sparkline graph + Item { + Layout.topMargin: Tokens.spacing.medium + Layout.bottomMargin: Tokens.spacing.small + Layout.fillWidth: true + Layout.fillHeight: true + + SparklineItem { + id: sparkline + + property real targetMax: 1024 + property real smoothMax: targetMax + + anchors.fill: parent + line1: NetworkUsage.uploadBuffer // qmllint disable missing-type + line1Color: Colours.palette.m3secondary + line1FillAlpha: 0.15 + line2: NetworkUsage.downloadBuffer // qmllint disable missing-type + line2Color: Colours.palette.m3tertiary + line2FillAlpha: 0.2 + maxValue: smoothMax + historyLength: NetworkUsage.historyLength + + Connections { + function onValuesChanged(): void { + sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); + slideAnim.restart(); + } + + target: NetworkUsage.downloadBuffer + } + + NumberAnimation { + id: slideAnim + + target: sparkline + property: "slideProgress" + from: 0 + to: 1 + easing.type: Easing.Linear + duration: GlobalConfig.dashboard.resourceUpdateInterval + } + + Behavior on smoothMax { + Anim {} + } + } + + // "Collecting data" placeholder + StyledText { + anchors.centerIn: parent + text: qsTr("Collecting data...") + font: Tokens.font.body.small + color: Colours.palette.m3outline + visible: NetworkUsage.downloadBuffer.count < 2 + } + } + + // Download row + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.small + + MaterialIcon { + text: "download" + color: Colours.palette.m3tertiary + fontStyle: Tokens.font.icon.medium + } + + StyledText { + text: qsTr("Download") + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); + return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; + } + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() + color: Colours.palette.m3tertiary + } + } + + // Upload row + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.small + + MaterialIcon { + text: "upload" + color: Colours.palette.m3secondary + fontStyle: Tokens.font.icon.medium + } + + StyledText { + text: qsTr("Upload") + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); + return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; + } + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() + color: Colours.palette.m3secondary + } + } + + // Session totals + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.small + + MaterialIcon { + text: "history" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.medium + } + + StyledText { + text: qsTr("Total") + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0); + const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0); + return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B"; + } + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + } + } +} diff --git a/modules/dashboard/performance/StorageCard.qml b/modules/dashboard/performance/StorageCard.qml new file mode 100644 index 00000000..f1d5723a --- /dev/null +++ b/modules/dashboard/performance/StorageCard.qml @@ -0,0 +1,135 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.components.controls +import qs.services + +StyledRect { + id: root + + readonly property color accent: Colours.palette.m3secondary + readonly property real percentage: Storage.primaryDisk?.perc ?? 0 + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.extraExtraLarge + + implicitWidth: layout.implicitWidth + layout.anchors.margins * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + + ServiceRef { + service: Storage + } + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.extraLarge + spacing: 0 + + RowLayout { + id: row + + Layout.alignment: Qt.AlignHCenter + spacing: Tokens.spacing.large + + CircularProgress { + fgColour: root.accent + value: root.percentage + implicitSize: usageColumn.implicitHeight + thickness + Tokens.padding.large * 2 + startAngle: -225 + sweepAngle: 270 + + Behavior on clampedVal { + Anim {} + } + + ColumnLayout { + id: usageColumn + + anchors.centerIn: parent + spacing: 0 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "hard_drive" + color: root.accent + fontStyle: Tokens.font.icon.medium + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Math.round(root.percentage * 100) + "%" + font: Tokens.font.title.builders.large.width(90).build() + color: root.accent + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Used") + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + } + } + + ColumnLayout { + Layout.minimumWidth: Tokens.sizes.dashboard.perfStorageTextWidth + spacing: Tokens.spacing.extraSmall + + StyledText { + text: qsTr("Storage") + font: Tokens.font.title.medium + } + + StyledText { + text: { + if (!Storage.primaryDisk) + return qsTr("No disks detected"); + + const fmt = UsageFmt.formatKib(Storage.primaryDisk.used, Storage.primaryDisk.total); + return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`; + } + font: Tokens.font.body.large + color: root.accent + } + } + } + + SplitButton { + Layout.alignment: Qt.AlignHCenter + + type: SplitButton.Tonal + disabled: !Storage.disks.length + fallbackIcon: "storage" + fallbackText: qsTr("No disks") + menuOnTop: true + minLeftWidth: row.implicitWidth * 0.6 + + menuItems: disks.instances + active: menuItems.find(m => m.modelData === Storage.primaryDisk) ?? menuItems[0] ?? null + menu.onItemSelected: item => Storage.manualPrimaryDisk = (item as DiskItem).modelData + + Variants { + id: disks + + model: Storage.disks + + DiskItem {} + } + } + } + + component DiskItem: MenuItem { + required property var modelData + + icon: modelData === Storage.primaryDisk ? "check" : "" + text: modelData.mount + activeIcon: "storage" + } +} diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 677c84a0..d2865fff 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -39,6 +39,8 @@ StyledWindow { readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg) readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness + property color surfaceColour: Colours.tPalette.m3surface + readonly property int dragMaskPadding: { if (focusGrab.active || panels.popouts.isDetached) return 0; @@ -63,7 +65,7 @@ StyledWindow { name: "drawers" WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: fsTransitionProg > 0 && contentItem.Config.general.showOverFullscreen ? WlrLayer.Overlay : WlrLayer.Top - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None mask: hasFullscreen ? emptyRegion : regions @@ -76,6 +78,10 @@ StyledWindow { Anim {} } + Behavior on surfaceColour { + CAnim {} + } + Region { id: emptyRegion @@ -117,17 +123,19 @@ StyledWindow { StyledRect { anchors.fill: parent - opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 + opacity: (visibilities.session && Config.session.enabled) || panels.popouts.detachedMode !== "" ? 0.5 : 0 color: Colours.palette.m3scrim Behavior on opacity { - Anim {} + Anim { + type: Anim.SlowEffects + } } } Item { anchors.fill: parent - opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 + opacity: root.surfaceColour.a layer.enabled: true layer.effect: MultiEffect { shadowEnabled: true @@ -138,12 +146,8 @@ StyledWindow { BlobGroup { id: blobGroup - color: Colours.palette.m3surface + color: root.surfaceColour smoothing: root.contentItem.Config.border.smoothing - - Behavior on color { - CAnim {} - } } BlobInvertedRect { @@ -226,9 +230,7 @@ StyledWindow { implicitWidth: panels.popouts.width * (1 + extraWidth) Behavior on extraWidth { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } @@ -312,7 +314,7 @@ StyledWindow { y: panel.y + root.borderThickness implicitWidth: panel.width implicitHeight: panel.height - radius: Tokens.rounding.large + radius: Tokens.rounding.extraLarge deformScale: (deformAmount * Config.appearance.deformScale) / 10000 } } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 52e20d26..eaeb4538 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -68,6 +68,7 @@ Item { sidebarPanel: sidebar osdPanel: osdWrapper sessionPanel: sessionWrapper + utilitiesPanel: utilities anchors.top: parent.top anchors.right: parent.right @@ -138,7 +139,7 @@ Item { anchors.bottom: sidebar.visible ? parent.bottom : utilities.top anchors.right: sidebar.left - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium } Sidebar.Wrapper { @@ -149,5 +150,6 @@ Item { anchors.top: notifications.bottom anchors.bottom: utilities.top anchors.right: parent.right + anchors.topMargin: -notifications.anchors.topMargin } } diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index a2109c04..6ef55df4 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -32,7 +32,7 @@ StyledListView { highlightFollowsCurrentItem: false highlight: StyledRect { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.palette.m3onSurface opacity: 0.08 @@ -41,9 +41,7 @@ StyledListView { implicitHeight: root.currentItem?.implicitHeight ?? 0 Behavior on y { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } @@ -167,7 +165,8 @@ StyledListView { enabled: !root.state Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" from: 0 to: 1 } @@ -177,7 +176,8 @@ StyledListView { enabled: !root.state Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" from: 1 to: 0 } @@ -188,7 +188,8 @@ StyledListView { property: "y" } Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" to: 1 } } @@ -199,7 +200,8 @@ StyledListView { type: Anim.StandardSmall } Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" to: 1 } } @@ -209,7 +211,8 @@ StyledListView { property: "y" } Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" to: 1 } } diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 69be0c3a..1c990388 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import Caelestia import Caelestia.Config import qs.components import qs.components.controls @@ -15,10 +16,10 @@ Item { required property real maxHeight readonly property int padding: Tokens.padding.large - readonly property int rounding: Tokens.rounding.large + readonly property int rounding: Tokens.rounding.extraLarge implicitWidth: listWrapper.width + padding * 2 - implicitHeight: searchWrapper.height + listWrapper.height + padding * 2 + implicitHeight: searchWrapper.height + listWrapper.height + padding + searchWrapper.anchors.bottomMargin Item { id: listWrapper @@ -53,6 +54,7 @@ Item { anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: root.padding + anchors.bottomMargin: CUtils.clamp(root.padding - Config.border.thickness, 0, root.padding) implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight) @@ -75,8 +77,8 @@ Item { anchors.leftMargin: Tokens.spacing.small anchors.rightMargin: Tokens.spacing.small - topPadding: Tokens.padding.larger - bottomPadding: Tokens.padding.larger + topPadding: Tokens.padding.medium + bottomPadding: Tokens.padding.medium placeholderText: qsTr("Type \"%1\" for commands").arg(GlobalConfig.launcher.actionPrefix) diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index db7abdf9..f42087ed 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -60,7 +60,7 @@ Item { property: "opacity" from: 1 to: 0 - type: Anim.StandardSmall + type: Anim.DefaultEffects } PropertyAction {} Anim { @@ -68,7 +68,7 @@ Item { property: "opacity" from: 0 to: 1 - type: Anim.StandardSmall + type: Anim.DefaultEffects } } } @@ -110,7 +110,7 @@ Item { opacity: root.currentList?.count === 0 ? 1 : 0 scale: root.currentList?.count === 0 ? 1 : 0.5 - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium padding: Tokens.padding.large anchors.horizontalCenter: parent.horizontalCenter @@ -119,7 +119,7 @@ Item { MaterialIcon { text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge anchors.verticalCenter: parent.verticalCenter } @@ -130,19 +130,20 @@ Item { StyledText { text: root.state === "wallpapers" ? qsTr("No wallpapers found") : qsTr("No results") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { text: root.state === "wallpapers" && Wallpapers.list.length === 0 ? qsTr("Try putting some wallpapers in %1").arg(Paths.shortenHome(Paths.wallsdir)) : qsTr("Try searching for something else") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -153,18 +154,12 @@ Item { Behavior on implicitWidth { enabled: root.visibilities.launcher - Anim { - duration: Tokens.anim.durations.large - easing: Tokens.anim.emphasizedDecel - } + Anim {} } Behavior on implicitHeight { enabled: root.visibilities.launcher - Anim { - duration: Tokens.anim.durations.large - easing: Tokens.anim.emphasizedDecel - } + Anim {} } } diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml index 3663f4bc..8f16e2a1 100644 --- a/modules/launcher/WallpaperList.qml +++ b/modules/launcher/WallpaperList.qml @@ -15,7 +15,7 @@ PathView { required property var panels required property var content - readonly property int itemWidth: Tokens.sizes.launcher.wallpaperWidth * 0.8 + Tokens.padding.larger * 2 + readonly property int itemWidth: Tokens.sizes.launcher.wallpaperWidth * 0.8 + Tokens.padding.medium * 2 readonly property int numItems: { const screen = (QsWindow.window as QsWindow)?.screen; diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index d5630b23..f8f87860 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -16,7 +16,7 @@ Item { readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled readonly property real maxHeight: { - let max = screen.height - Config.border.thickness * 2 - Tokens.spacing.large; + let max = screen.height - Config.border.thickness * 2 + Tokens.padding.extraLarge; if (visibilities.dashboard) max -= panels.dashboard.nonAnimHeight; return max; @@ -40,9 +40,7 @@ Item { Component.onCompleted: Qt.callLater(() => Apps) // Load apps on init Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index 0f9fb5dd..b7bbebbb 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -15,28 +15,28 @@ Item { anchors.right: parent?.right StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: root.modelData?.onClicked(root.list) } Item { anchors.fill: parent - anchors.leftMargin: Tokens.padding.larger - anchors.rightMargin: Tokens.padding.larger - anchors.margins: Tokens.padding.smaller + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + anchors.margins: Tokens.padding.small MaterialIcon { id: icon - text: root.modelData?.icon ?? "" - font.pointSize: Tokens.font.size.extraLarge - anchors.verticalCenter: parent.verticalCenter + text: root.modelData?.icon ?? "" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.builders.large.scale(1.3).build() } Item { anchors.left: icon.right - anchors.leftMargin: Tokens.spacing.normal + anchors.leftMargin: Tokens.spacing.medium anchors.verticalCenter: icon.verticalCenter implicitWidth: parent.width - icon.width @@ -46,18 +46,18 @@ Item { id: name text: root.modelData?.name ?? "" - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } StyledText { id: desc text: root.modelData?.desc ?? "" - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3outline elide: Text.ElideRight - width: root.width - icon.width - Tokens.rounding.normal * 2 + width: root.width - icon.width - Tokens.rounding.extraLargeIncreased anchors.top: name.bottom } diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 80739c8f..ba6b0aab 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -19,7 +19,7 @@ Item { anchors.right: parent?.right StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: { Apps.launch(root.modelData); root.visibilities.launcher = false; @@ -28,9 +28,9 @@ Item { Item { anchors.fill: parent - anchors.leftMargin: Tokens.padding.larger - anchors.rightMargin: Tokens.padding.larger - anchors.margins: Tokens.padding.smaller + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + anchors.margins: Tokens.padding.small IconImage { id: icon @@ -44,7 +44,7 @@ Item { Item { anchors.left: icon.right - anchors.leftMargin: Tokens.spacing.normal + anchors.leftMargin: Tokens.spacing.medium anchors.verticalCenter: icon.verticalCenter implicitWidth: parent.width - icon.width - favouriteIcon.width @@ -54,18 +54,18 @@ Item { id: name text: root.modelData?.name ?? "" - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } StyledText { id: comment text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? "" - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3outline elide: Text.ElideRight - width: root.width - icon.width - favouriteIcon.width - Tokens.rounding.normal * 2 + width: root.width - icon.width - favouriteIcon.width - Tokens.rounding.extraLargeIncreased anchors.top: name.bottom } diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index f746f96c..38097123 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -28,7 +28,7 @@ Item { anchors.right: parent?.right StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: root.onClicked() } @@ -36,13 +36,13 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.larger + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { text: "function" - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge Layout.alignment: Qt.AlignVCenter } @@ -66,11 +66,11 @@ Item { StyledRect { color: Colours.palette.m3tertiary - radius: Tokens.rounding.normal + radius: Tokens.rounding.large clip: true - implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Tokens.padding.small * 2 + implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Tokens.padding.small Layout.alignment: Qt.AlignVCenter @@ -94,12 +94,14 @@ Item { text: qsTr("Open in calculator") color: Colours.palette.m3onTertiary - font.pointSize: Tokens.font.size.normal + font: Tokens.font.label.medium opacity: stateLayer.containsMouse ? 1 : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -108,11 +110,11 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.medium text: "open_in_new" color: Colours.palette.m3onTertiary - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } Behavior on implicitWidth { diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index ab0784bd..a2f466cc 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -16,15 +16,15 @@ Item { anchors.right: parent?.right StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: root.modelData?.onClicked(root.list) } Item { anchors.fill: parent - anchors.leftMargin: Tokens.padding.larger - anchors.rightMargin: Tokens.padding.larger - anchors.margins: Tokens.padding.smaller + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + anchors.margins: Tokens.padding.small StyledRect { id: preview @@ -61,20 +61,20 @@ Item { Column { anchors.left: preview.right - anchors.leftMargin: Tokens.spacing.normal + anchors.leftMargin: Tokens.spacing.medium anchors.verticalCenter: parent.verticalCenter - width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) + width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.medium : 0) spacing: 0 StyledText { text: root.modelData?.flavour ?? "" - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } StyledText { text: root.modelData?.name ?? "" - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3outline elide: Text.ElideRight @@ -95,7 +95,7 @@ Item { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index b13d5d8e..fa3fb259 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -16,41 +16,41 @@ Item { anchors.right: parent?.right StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: root.modelData?.onClicked(root.list) } Item { anchors.fill: parent - anchors.leftMargin: Tokens.padding.larger - anchors.rightMargin: Tokens.padding.larger - anchors.margins: Tokens.padding.smaller + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + anchors.margins: Tokens.padding.small MaterialIcon { id: icon text: root.modelData?.icon ?? "" - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge anchors.verticalCenter: parent.verticalCenter } Column { anchors.left: icon.right - anchors.leftMargin: Tokens.spacing.larger + anchors.leftMargin: Tokens.spacing.large anchors.verticalCenter: icon.verticalCenter - width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) + width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.medium : 0) spacing: 0 StyledText { text: root.modelData?.name ?? "" - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } StyledText { text: root.modelData?.description ?? "" - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3outline elide: Text.ElideRight @@ -71,7 +71,7 @@ Item { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index d09a894c..9f87cba7 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -22,11 +22,11 @@ Item { opacity = Qt.binding(() => PathView.onPath ? 1 : 0); } - implicitWidth: image.width + Tokens.padding.larger * 2 - implicitHeight: image.height + label.height + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal + implicitWidth: image.width + Tokens.padding.medium * 2 + implicitHeight: image.height + label.height + Tokens.spacing.extraSmall + Tokens.padding.large + Tokens.padding.medium StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: { Wallpapers.setWallpaper(root.modelData.path); root.visibilities.launcher = false; @@ -40,7 +40,9 @@ Item { level: 4 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -50,7 +52,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter y: Tokens.padding.large color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.normal + radius: Tokens.rounding.large implicitWidth: Tokens.sizes.launcher.wallpaperWidth implicitHeight: implicitWidth / 16 * 9 @@ -59,8 +61,7 @@ Item { anchors.centerIn: parent text: "image" color: Colours.tPalette.m3outline - font.pointSize: Tokens.font.size.extraLarge * 2 - font.weight: 600 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build() } CachingImage { @@ -78,15 +79,15 @@ Item { id: label anchors.top: image.bottom - anchors.topMargin: Tokens.spacing.small / 2 + anchors.topMargin: Tokens.spacing.extraSmall anchors.horizontalCenter: parent.horizontalCenter - width: image.width - Tokens.padding.normal * 2 + width: image.width - Tokens.padding.medium * 2 horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight renderType: Text.QtRendering text: root.modelData.relativePath - font.pointSize: Tokens.font.size.normal + font: Tokens.font.label.medium } Behavior on scale { @@ -94,6 +95,8 @@ Item { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index a987d4eb..c96c5f0c 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -1,13 +1,9 @@ -pragma ComponentBehavior: Bound - +import "center" import QtQuick import QtQuick.Layouts import Caelestia.Config import qs.components -import qs.components.controls -import qs.components.images import qs.services -import qs.utils ColumnLayout { id: root @@ -20,396 +16,37 @@ ColumnLayout { Layout.fillWidth: false Layout.fillHeight: true - spacing: Tokens.spacing.large * 2 + spacing: Tokens.spacing.largeIncreased - RowLayout { + Clock { Layout.alignment: Qt.AlignHCenter - spacing: Tokens.spacing.small - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: Time.hourStr - color: Colours.palette.m3secondary - font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) - font.family: Tokens.font.family.clock - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: ":" - color: Colours.palette.m3primary - font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) - font.family: Tokens.font.family.clock - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: Time.minuteStr - color: Colours.palette.m3secondary - font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) - font.family: Tokens.font.family.clock - font.bold: true - } - - Loader { - asynchronous: true - Layout.leftMargin: Tokens.spacing.small - Layout.alignment: Qt.AlignVCenter - - active: GlobalConfig.services.useTwelveHourClock - visible: active - - sourceComponent: StyledText { - text: Time.amPmStr - color: Colours.palette.m3primary - font.pointSize: Math.floor(Tokens.font.size.extraLarge * 2 * root.centerScale) - font.family: Tokens.font.family.clock - font.bold: true - } - } + Layout.topMargin: Tokens.padding.large + centerScale: root.centerScale } StyledText { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: -Tokens.padding.large * 2 - text: Time.format("dddd, d MMMM yyyy") - color: Colours.palette.m3tertiary - font.pointSize: Math.floor(Tokens.font.size.extraLarge * root.centerScale) - font.family: Tokens.font.family.mono - font.bold: true + text: Time.format("dddd • d MMM").toUpperCase() + color: Colours.palette.m3onSurface + font: Tokens.font.title.builders.medium.weight(Font.DemiBold).build() } - StyledClippingRect { - Layout.topMargin: Tokens.spacing.large * 2 + ProfilePic { Layout.alignment: Qt.AlignHCenter - - implicitWidth: root.centerWidth / 2 - implicitHeight: root.centerWidth / 2 - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.full - - MaterialIcon { - anchors.centerIn: parent - - text: "person" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Math.floor(root.centerWidth / 4) - visible: pfp.status !== Image.Ready - } - - CachingImage { - id: pfp - - anchors.fill: parent - path: `${Paths.home}/.face` - } + Layout.topMargin: Tokens.spacing.extraExtraLarge * root.centerScale + Layout.bottomMargin: Tokens.spacing.extraLarge * root.centerScale + centerWidth: root.centerWidth } - StyledRect { + PasswordInput { Layout.alignment: Qt.AlignHCenter - - implicitWidth: root.centerWidth * 0.8 - implicitHeight: input.implicitHeight + Tokens.padding.small * 2 - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.full - - focus: true - onActiveFocusChanged: { - if (!activeFocus) - forceActiveFocus(); - } - - Keys.onPressed: event => { - if (root.lock.unlocking) - return; - - if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) - inputField.placeholder.animate = false; - - root.lock.pam.handleKey(event); - } - - StateLayer { - onClicked: { - parent.forceActiveFocus(); - } - - hoverEnabled: false - cursorShape: Qt.IBeamCursor - } - - RowLayout { - id: input - - anchors.fill: parent - anchors.margins: Tokens.padding.small - spacing: Tokens.spacing.normal - - Item { - implicitWidth: implicitHeight - implicitHeight: fprintIcon.implicitHeight + Tokens.padding.small * 2 - - MaterialIcon { - id: fprintIcon - - anchors.centerIn: parent - animate: true - text: { - if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries) - return "fingerprint_off"; - if (root.lock.pam.fprint.active) - return "fingerprint"; - return "lock"; - } - color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface - opacity: root.lock.pam.passwd.active ? 0 : 1 - - Behavior on opacity { - Anim {} - } - } - - CircularIndicator { - anchors.fill: parent - running: root.lock.pam.passwd.active - } - } - - InputField { - id: inputField - - pam: root.lock.pam - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: enterIcon.implicitHeight + Tokens.padding.small * 2 - - color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Tokens.rounding.full - - StateLayer { - color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - onClicked: root.lock.pam.passwd.start() - } - - MaterialIcon { - id: enterIcon - - anchors.centerIn: parent - text: "arrow_forward" - color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - font.weight: 500 - } - } - } + centerWidth: root.centerWidth + lock: root.lock } - Item { + StateMessage { Layout.fillWidth: true - Layout.topMargin: -Tokens.spacing.large - - implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) - - Behavior on implicitHeight { - Anim {} - } - - StyledText { - id: stateMessage - - readonly property string msg: { - if (Hypr.kbLayout !== Hypr.defaultKbLayout) { - if (Hypr.capsLock && Hypr.numLock) - return qsTr("Caps lock and Num lock are ON.\nKeyboard layout: %1").arg(Hypr.kbLayoutFull); - if (Hypr.capsLock) - return qsTr("Caps lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); - if (Hypr.numLock) - return qsTr("Num lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); - return qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull); - } - - if (Hypr.capsLock && Hypr.numLock) - return qsTr("Caps lock and Num lock are ON."); - if (Hypr.capsLock) - return qsTr("Caps lock is ON."); - if (Hypr.numLock) - return qsTr("Num lock is ON."); - - return ""; - } - - property bool shouldBeVisible - - onMsgChanged: { - if (msg) { - if (opacity > 0) { - animate = true; - text = msg; - animate = false; - } else { - text = msg; - } - shouldBeVisible = true; - } else { - shouldBeVisible = false; - } - } - - anchors.left: parent.left - anchors.right: parent.right - - scale: shouldBeVisible && !message.msg ? 1 : 0.7 - opacity: shouldBeVisible && !message.msg ? 1 : 0 - color: Colours.palette.m3onSurfaceVariant - animateProp: "opacity" - - font.family: Tokens.font.family.mono - horizontalAlignment: Qt.AlignHCenter - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - lineHeight: 1.2 - - Behavior on scale { - Anim {} - } - - Behavior on opacity { - Anim {} - } - } - - StyledText { - id: message - - readonly property Pam pam: root.lock.pam - readonly property string msg: { - if (pam.fprintState === "error") - return qsTr("FP ERROR: %1").arg(pam.fprint.message); - if (pam.state === "error") - return qsTr("PW ERROR: %1").arg(pam.passwd.message); - - if (pam.lockMessage) - return pam.lockMessage; - - if (pam.state === "max" && pam.fprintState === "max") - return qsTr("Maximum password and fingerprint attempts reached."); - if (pam.state === "max") { - if (pam.fprint.available) - return qsTr("Maximum password attempts reached. Please use fingerprint."); - return qsTr("Maximum password attempts reached."); - } - if (pam.fprintState === "max") - return qsTr("Maximum fingerprint attempts reached. Please use password."); - - if (pam.state === "fail") { - if (pam.fprint.available) - return qsTr("Incorrect password. Please try again or use fingerprint."); - return qsTr("Incorrect password. Please try again."); - } - if (pam.fprintState === "fail") - return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries); - - return ""; - } - - anchors.left: parent.left - anchors.right: parent.right - - scale: 0.7 - opacity: 0 - color: Colours.palette.m3error - - font.pointSize: Tokens.font.size.small - font.family: Tokens.font.family.mono - horizontalAlignment: Qt.AlignHCenter - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - - onMsgChanged: { - if (msg) { - if (opacity > 0) { - animate = true; - text = msg; - animate = false; - - exitAnim.stop(); - if (scale < 1) - appearAnim.restart(); - else - flashAnim.restart(); - } else { - text = msg; - exitAnim.stop(); - appearAnim.restart(); - } - } else { - appearAnim.stop(); - flashAnim.stop(); - exitAnim.start(); - } - } - - Connections { - function onFlashMsg(): void { - exitAnim.stop(); - if (message.scale < 1) - appearAnim.restart(); - else - flashAnim.restart(); - } - - target: root.lock.pam - } - - Anim { - id: appearAnim - - target: message - properties: "scale,opacity" - to: 1 - onFinished: flashAnim.restart() - } - - SequentialAnimation { - id: flashAnim - - loops: 2 - - FlashAnim { - to: 0.3 - } - FlashAnim { - to: 1 - } - } - - ParallelAnimation { - id: exitAnim - - Anim { - target: message - property: "scale" - to: 0.7 - type: Anim.StandardLarge - } - Anim { - target: message - property: "opacity" - to: 0 - type: Anim.StandardLarge - } - } - } - } - - component FlashAnim: NumberAnimation { - target: message - property: "opacity" - duration: Tokens.anim.durations.small - easing.type: Easing.Linear + pam: root.lock.pam } } diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml index 0ff7daad..0d47502a 100644 --- a/modules/lock/Content.qml +++ b/modules/lock/Content.qml @@ -9,50 +9,26 @@ RowLayout { required property var lock - spacing: Tokens.spacing.large * 2 + spacing: Tokens.spacing.largeIncreased * 2 ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium - StyledRect { + WeatherInfo { Layout.fillWidth: true - implicitHeight: weather.implicitHeight - - topLeftRadius: Tokens.rounding.large - radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - WeatherInfo { - id: weather - - rootHeight: root.height - } + rootHeight: root.height } - StyledRect { + Fetch { + Layout.fillWidth: true + rootHeight: root.height + } + + Media { Layout.fillWidth: true Layout.fillHeight: true - - radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - Fetch {} - } - - StyledClippingRect { - Layout.fillWidth: true - implicitHeight: media.implicitHeight - - bottomLeftRadius: Tokens.rounding.large - radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - Media { - id: media - - lock: root.lock - } + lock: root.lock } } @@ -62,27 +38,18 @@ RowLayout { ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium - StyledRect { + Resources { Layout.fillWidth: true - implicitHeight: resources.implicitHeight - - topRightRadius: Tokens.rounding.large - radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - Resources { - id: resources - } } StyledRect { Layout.fillWidth: true Layout.fillHeight: true - bottomRightRadius: Tokens.rounding.large - radius: Tokens.rounding.small + bottomRightRadius: Tokens.rounding.extraLarge + radius: Tokens.rounding.medium color: Colours.tPalette.m3surfaceContainer NotifDock { diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml index 0afbe455..0094b5f0 100644 --- a/modules/lock/Fetch.qml +++ b/modules/lock/Fetch.qml @@ -3,140 +3,147 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell.Services.UPower +import Caelestia import Caelestia.Config import qs.components import qs.components.effects import qs.services import qs.utils -ColumnLayout { +StyledRect { id: root - anchors.fill: parent - anchors.margins: Tokens.padding.large * 2 - anchors.topMargin: Tokens.padding.large + required property real rootHeight + readonly property int cBoxSize: Tokens.font.body.medium.pointSize * 2 - spacing: Tokens.spacing.small + implicitHeight: layout.implicitHeight + layout.anchors.topMargin + layout.anchors.margins + radius: Tokens.rounding.medium + color: Colours.tPalette.m3surfaceContainer - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: false - spacing: Tokens.spacing.normal + ColumnLayout { + id: layout - StyledRect { - implicitWidth: prompt.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: prompt.implicitHeight + Tokens.padding.normal * 2 + anchors.fill: parent + anchors.margins: Tokens.padding.extraLarge + anchors.topMargin: Tokens.padding.extraLarge + anchors.bottomMargin: Tokens.padding.extraLarge - color: Colours.palette.m3primary - radius: Tokens.rounding.small + spacing: Tokens.spacing.small + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: false + spacing: Tokens.spacing.medium + + StyledRect { + implicitWidth: prompt.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: prompt.implicitHeight + Tokens.padding.small * 2 + + color: Colours.palette.m3primary + radius: Tokens.rounding.medium + + MonoText { + id: prompt + + anchors.centerIn: parent + text: ">" + color: Colours.palette.m3onPrimary + } + } MonoText { - id: prompt + Layout.fillWidth: true + text: "caelestiafetch.sh" + elide: Text.ElideRight + } - anchors.centerIn: parent - text: ">" - font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal - color: Colours.palette.m3onPrimary + WrappedLoader { + Layout.fillHeight: true + Layout.preferredWidth: height + Layout.preferredHeight: 0 + active: !iconLoader.active + + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } } - MonoText { + RowLayout { Layout.fillWidth: true - text: "caelestiafetch.sh" - font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal - elide: Text.ElideRight + Layout.fillHeight: true + spacing: Tokens.spacing.extraLarge + + WrappedLoader { + id: iconLoader + + Layout.fillHeight: true + active: root.width > Tokens.sizes.lock.largeLogoWidth + + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: Tokens.padding.medium + Layout.bottomMargin: iconLoader.active || colourRowLoader.active ? Tokens.padding.medium : 0 + spacing: Tokens.spacing.medium + + Repeater { + model: { + const items = []; + const hasBatt = UPower.displayDevice.isLaptopBattery; + const rHeight = root.rootHeight; + + if (!hasBatt && rHeight > Tokens.sizes.lock.fetch4LinesHeight) + items.push(`OS : ${SysInfo.osPrettyName || SysInfo.osName}`); + + if (rHeight > (hasBatt ? Tokens.sizes.lock.fetch4LinesHeight : Tokens.sizes.lock.fetch3LinesHeight)) + items.push(`WM : ${SysInfo.wm}`); + + if (!hasBatt || rHeight > Tokens.sizes.lock.fetch3LinesHeight) + items.push(`USER: ${SysInfo.user}`); + + items.push(`UP : ${SysInfo.uptime}`); + + if (hasBatt) + items.push(`BATT: ${[UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state) ? "(+) " : ""}${Math.round(UPower.displayDevice.percentage * 100)}%`); + + return items; + } + + MonoText { + required property string modelData + + Layout.fillWidth: true + text: modelData + elide: Text.ElideRight + } + } + } } WrappedLoader { - Layout.fillHeight: true - active: !iconLoader.active + id: colourRowLoader - sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon - } - } + Layout.topMargin: iconLoader.active ? Tokens.spacing.small : 0 + Layout.alignment: Qt.AlignHCenter + active: root.rootHeight > Tokens.sizes.lock.showColourBoxRowHeight - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: false - spacing: height * 0.15 + sourceComponent: RowLayout { + id: coloursRow - WrappedLoader { - id: iconLoader + spacing: Tokens.spacing.largeIncreased - Layout.fillHeight: true - active: root.width > 320 + Repeater { + model: CUtils.clamp(Math.floor((layout.width + coloursRow.spacing) / (root.cBoxSize + coloursRow.spacing)), 0, 8) - sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon - } + StyledRect { + required property int index - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: Tokens.padding.normal - Layout.bottomMargin: Tokens.padding.normal - Layout.leftMargin: iconLoader.active ? 0 : width * 0.1 - spacing: Tokens.spacing.normal - - WrappedLoader { - Layout.fillWidth: true - active: !batLoader.active && root.height > 200 - - sourceComponent: FetchText { - text: `OS : ${SysInfo.osPrettyName || SysInfo.osName}` - } - } - - WrappedLoader { - Layout.fillWidth: true - active: root.height > (batLoader.active ? 200 : 110) - - sourceComponent: FetchText { - text: `WM : ${SysInfo.wm}` - } - } - - WrappedLoader { - Layout.fillWidth: true - active: !batLoader.active || root.height > 110 - - sourceComponent: FetchText { - text: `USER: ${SysInfo.user}` - } - } - - FetchText { - text: `UP : ${SysInfo.uptime}` - } - - WrappedLoader { - id: batLoader - - Layout.fillWidth: true - active: UPower.displayDevice.isLaptopBattery - - sourceComponent: FetchText { - text: `BATT: ${[UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state) ? "(+) " : ""}${Math.round(UPower.displayDevice.percentage * 100)}%` - } - } - } - } - - WrappedLoader { - Layout.alignment: Qt.AlignHCenter - active: root.height > 180 - - sourceComponent: RowLayout { - spacing: Tokens.spacing.large - - Repeater { - model: Math.max(0, Math.min(8, root.width / (Tokens.font.size.larger * 2 + Tokens.spacing.large))) - - StyledRect { - required property int index - - implicitWidth: implicitHeight - implicitHeight: Tokens.font.size.larger * 2 - color: Colours.palette[`term${index}`] - radius: Tokens.rounding.small + implicitWidth: implicitHeight + implicitHeight: root.cBoxSize + color: Colours.palette[`term${index}`] + radius: Tokens.rounding.medium + } } } } @@ -166,13 +173,7 @@ ColumnLayout { visible: active } - component FetchText: MonoText { - Layout.fillWidth: true - font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal - elide: Text.ElideRight - } - component MonoText: StyledText { - font.family: Tokens.font.family.mono + font: root.width > Tokens.sizes.lock.largeFontWidth ? Tokens.font.mono.medium : Tokens.font.mono.small } } diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index 322773fd..2c97248d 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -36,7 +36,6 @@ WlSessionLockSurface { target: lockContent properties: "implicitWidth,implicitHeight" to: lockContent.size - type: Anim.DefaultSpatial } Anim { target: lockBg @@ -47,7 +46,6 @@ WlSessionLockSurface { target: content property: "scale" to: 0 - type: Anim.DefaultSpatial } Anim { target: content @@ -72,6 +70,7 @@ WlSessionLockSurface { duration: Tokens.anim.durations.small } Anim { + type: Anim.Standard target: lockContent property: "opacity" to: 0 @@ -120,11 +119,13 @@ WlSessionLockSurface { easing: Tokens.anim.standardDecel } Anim { + type: Anim.DefaultEffects target: lockIcon property: "opacity" to: 0 } Anim { + type: Anim.DefaultEffects target: content property: "opacity" to: 1 @@ -133,24 +134,21 @@ WlSessionLockSurface { target: content property: "scale" to: 1 - type: Anim.DefaultSpatial } Anim { target: lockBg property: "radius" - to: lockContent.Tokens.rounding.large * 1.5 + to: lockContent.Tokens.rounding.extraLarge * 1.5 } Anim { target: lockContent property: "implicitWidth" to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult * lockContent.Tokens.sizes.lock.ratio - type: Anim.DefaultSpatial } Anim { target: lockContent property: "implicitHeight" to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult - type: Anim.DefaultSpatial } } } @@ -207,8 +205,7 @@ WlSessionLockSurface { anchors.centerIn: parent text: "lock" - font.pointSize: Tokens.font.size.extraLarge * 4 - font.bold: true + fontStyle: Tokens.font.icon.builders.extraLarge.scale(4).weight(Font.Bold).build() rotation: 180 } @@ -216,8 +213,8 @@ WlSessionLockSurface { id: content anchors.centerIn: parent - width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2 - height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2 + width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.extraLargeIncreased + height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.extraLargeIncreased lock: root opacity: 0 diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index c55333c7..3a8dc794 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -1,23 +1,23 @@ -pragma ComponentBehavior: Bound - import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Components import Caelestia.Config import qs.components -import qs.components.effects +import qs.components.controls +import qs.components.images import qs.services -Item { +StyledClippingRect { id: root required property var lock - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: layout.implicitHeight + implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 + radius: Tokens.rounding.extraLarge + color: Colours.tPalette.m3surfaceContainer - Image { + FadeImage { anchors.fill: parent source: Players.getArtUrl(Players.active) @@ -29,12 +29,14 @@ Item { } layer.enabled: true - layer.effect: OpacityMask { - maskSource: mask - } - opacity: status === Image.Ready ? 1 : 0 + StyledRect { + anchors.fill: parent + color: Colours.palette.m3surface + opacity: 0.7 + } + Behavior on opacity { Anim { type: Anim.StandardExtraLarge @@ -42,160 +44,67 @@ Item { } } - Rectangle { - id: mask - - anchors.fill: parent - layer.enabled: true - visible: false - - gradient: Gradient { - orientation: Gradient.Horizontal - - GradientStop { - position: 0 - color: Qt.rgba(0, 0, 0, 0.5) - } - GradientStop { - position: 0.4 - color: Qt.rgba(0, 0, 0, 0.2) - } - GradientStop { - position: 0.8 - color: Qt.rgba(0, 0, 0, 0) - } - } - } - ColumnLayout { id: layout anchors.left: parent.left anchors.right: parent.right - anchors.margins: Tokens.padding.large - - StyledText { - Layout.topMargin: Tokens.padding.large - Layout.bottomMargin: Tokens.spacing.larger - text: qsTr("Now playing") - color: Colours.palette.m3onSurfaceVariant - font.family: Tokens.font.family.mono - font.weight: 500 - } + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.extraLarge + spacing: Tokens.spacing.extraSmall StyledText { Layout.fillWidth: true animate: true - text: Players.active?.trackArtist ?? qsTr("No media") + text: (Players.active?.trackTitle ?? qsTr("Nothing playing")) || qsTr("Unknown track") color: Colours.palette.m3primary horizontalAlignment: Text.AlignHCenter - font.pointSize: Tokens.font.size.large - font.family: Tokens.font.family.mono - font.weight: 600 + font: Tokens.font.title.medium elide: Text.ElideRight } StyledText { Layout.fillWidth: true animate: true - text: Players.active?.trackTitle ?? qsTr("No media") + text: (Players.active?.trackArtist ?? qsTr("Try playing some music!")) || qsTr("Unknown artist") + color: Colours.palette.m3onSurfaceVariant horizontalAlignment: Text.AlignHCenter - font.pointSize: Tokens.font.size.larger - font.family: Tokens.font.family.mono + font: Tokens.font.body.small elide: Text.ElideRight } - RowLayout { + ButtonRow { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Tokens.spacing.large * 1.2 - Layout.bottomMargin: Tokens.padding.large + Layout.topMargin: Tokens.spacing.medium - spacing: Tokens.spacing.large + spacing: Tokens.spacing.extraSmall - PlayerControl { + IconButton { + type: IconButton.Tonal icon: "skip_previous" - onClicked: { - if (Players.active?.canGoPrevious) - Players.active.previous(); - } + isRound: true + shapeMorph: true + disabled: !Players.active?.canGoPrevious + onClicked: Players.active?.previous() } - PlayerControl { - animate: true - icon: active ? "pause" : "play_arrow" - colour: "Primary" - level: active ? 2 : 1 - active: Players.active?.isPlaying ?? false - onClicked: { - if (Players.active?.canTogglePlaying) - Players.active.togglePlaying(); - } + IconButton { + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + isRound: true + shapeMorph: true + checked: Players.active?.isPlaying ?? false + disabled: !Players.active?.canTogglePlaying + onClicked: Players.active?.togglePlaying() + implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2 } - PlayerControl { + IconButton { + type: IconButton.Tonal icon: "skip_next" - onClicked: { - if (Players.active?.canGoNext) - Players.active.next(); - } - } - } - } - - component PlayerControl: StyledRect { - id: control - - property alias animate: controlIcon.animate - property alias icon: controlIcon.text - property bool active - property string colour: "Secondary" - property int level: 1 - - signal clicked - - Layout.preferredWidth: implicitWidth + (controlState.pressed ? Tokens.padding.normal * 2 : active ? Tokens.padding.small * 2 : 0) - implicitWidth: controlIcon.implicitWidth + Tokens.padding.large * 2 - implicitHeight: controlIcon.implicitHeight + Tokens.padding.normal * 2 - - color: active ? Colours.palette[`m3${colour.toLowerCase()}`] : Colours.palette[`m3${colour.toLowerCase()}Container`] - radius: active || controlState.pressed ? Tokens.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale) - - Elevation { - anchors.fill: parent - radius: parent.radius - z: -1 - level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level - } - - StateLayer { - id: controlState - - color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] - onClicked: control.clicked() - } - - MaterialIcon { - id: controlIcon - - anchors.centerIn: parent - color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] - font.pointSize: Tokens.font.size.large - fill: control.active ? 1 : 0 - - Behavior on fill { - Anim {} - } - } - - Behavior on Layout.preferredWidth { - Anim { - type: Anim.FastSpatial - } - } - - Behavior on radius { - Anim { - type: Anim.FastSpatial + isRound: true + shapeMorph: true + disabled: !Players.active?.canGoNext + onClicked: Players.active?.next() } } } diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 2254dfe0..ed5a396d 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -19,14 +19,13 @@ ColumnLayout { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.mono.builders.small.weight(Font.Medium).build() elide: Text.ElideRight } @@ -36,7 +35,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: "transparent" Loader { @@ -46,7 +45,7 @@ ColumnLayout { opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1 sourceComponent: ColumnLayout { - spacing: Tokens.spacing.large + spacing: Tokens.spacing.largeIncreased Image { asynchronous: true @@ -65,9 +64,7 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter text: Config.lock.hideNotifs ? qsTr("Unlock for Notifications") : qsTr("No Notifications") color: Colours.palette.m3outlineVariant - font.pointSize: Tokens.font.size.large - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.mono.builders.large.weight(Font.Medium).build() } } @@ -95,6 +92,7 @@ ColumnLayout { add: Transition { Anim { + type: Anim.DefaultEffects property: "opacity" from: 0 to: 1 @@ -103,12 +101,12 @@ ColumnLayout { property: "scale" from: 0 to: 1 - type: Anim.DefaultSpatial } } remove: Transition { Anim { + type: Anim.DefaultEffects property: "opacity" to: 0 } @@ -120,23 +118,23 @@ ColumnLayout { move: Transition { Anim { + type: Anim.DefaultEffects properties: "opacity,scale" to: 1 } Anim { property: "y" - type: Anim.DefaultSpatial } } displaced: Transition { Anim { + type: Anim.DefaultEffects properties: "opacity,scale" to: 1 } Anim { property: "y" - type: Anim.DefaultSpatial } } } diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 971b921a..2337376b 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -46,10 +46,10 @@ StyledRect { anchors.left: parent?.left anchors.right: parent?.right - implicitHeight: content.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: content.implicitHeight + Tokens.padding.medium * 2 clip: true - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: root.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) RowLayout { @@ -58,9 +58,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop @@ -101,7 +101,7 @@ StyledRect { MaterialIcon { text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) color: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } @@ -142,21 +142,21 @@ StyledRect { } ColumnLayout { - Layout.topMargin: -Tokens.padding.small - Layout.bottomMargin: -Tokens.padding.small / 2 - (root.expanded ? 0 : spacing) + Layout.topMargin: -Tokens.padding.extraSmall + Layout.bottomMargin: -Tokens.padding.extraSmall / 2 - (root.expanded ? 0 : spacing) Layout.fillWidth: true - spacing: Math.round(Tokens.spacing.small / 2) + spacing: Math.round(Tokens.spacing.extraSmall) RowLayout { Layout.bottomMargin: -parent.spacing Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true text: root.modelData color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small elide: Text.ElideRight } @@ -164,12 +164,12 @@ StyledRect { animate: true text: root.notifs[0]?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledRect { - implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 - implicitHeight: groupCount.implicitHeight + Tokens.padding.small + implicitWidth: expandBtn.implicitWidth + Tokens.padding.large + implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall color: root.urgency === "critical" ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) radius: Tokens.rounding.full @@ -186,20 +186,20 @@ StyledRect { id: expandBtn anchors.centerIn: parent - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { id: groupCount - Layout.leftMargin: Tokens.padding.small / 2 + Layout.leftMargin: Tokens.padding.extraSmall / 2 animate: true text: root.notifs.length color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } MaterialIcon { - Layout.rightMargin: -Tokens.padding.small / 2 + Layout.rightMargin: -Tokens.padding.extraSmall / 2 animate: true text: root.expanded ? "expand_less" : "expand_more" color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface @@ -207,7 +207,9 @@ StyledRect { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on Layout.preferredWidth { @@ -228,6 +230,7 @@ StyledRect { running: true Anim { + type: Anim.DefaultEffects target: notif property: "opacity" from: 0 @@ -252,6 +255,7 @@ StyledRect { onFinished: notif.modelData.unlock(notif) Anim { + type: Anim.DefaultEffects target: notif property: "opacity" to: 0 @@ -289,16 +293,16 @@ StyledRect { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } component NotifLine: StyledText { @@ -331,8 +335,7 @@ StyledRect { id: metrics text: `${notifLine.modelData.summary} ${notifLine.modelData.body}`.replace(/\n/g, " ") - font.pointSize: notifLine.font.pointSize - font.family: notifLine.font.family + font: notifLine.font elideWidth: notifLine.width elide: Text.ElideRight } diff --git a/modules/lock/Resources.qml b/modules/lock/Resources.qml index 1f38aa00..caa39e5b 100644 --- a/modules/lock/Resources.qml +++ b/modules/lock/Resources.qml @@ -1,93 +1,183 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Layouts +import M3Shapes import Caelestia.Config +import Caelestia.Services import qs.components -import qs.components.controls -import qs.components.misc +import qs.components.effects +import qs.components.widgets import qs.services -GridLayout { +StyledRect { id: root - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Tokens.padding.large - - rowSpacing: Tokens.spacing.large - columnSpacing: Tokens.spacing.large - rows: 2 - columns: 2 - - Ref { - service: SystemUsage + readonly property real fontScale: { + const diff = width / 391 - 1; // 391 is the width at 1080 height screen + return 1 + Math.pow(Math.abs(diff), 0.8) * Math.sign(diff); } - Resource { - Layout.topMargin: Tokens.padding.large - icon: "memory" - value: SystemUsage.cpuPerc - colour: Colours.palette.m3primary + implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 + radius: Tokens.rounding.extraLarge + color: Colours.tPalette.m3surfaceContainer + + ServiceRef { + service: Cpu } - Resource { - Layout.topMargin: Tokens.padding.large - icon: "thermostat" - value: Math.min(1, SystemUsage.cpuTemp / 90) - colour: Colours.palette.m3secondary + ServiceRef { + service: Memory } - Resource { - Layout.bottomMargin: Tokens.padding.large - icon: "memory_alt" - value: SystemUsage.memPerc - colour: Colours.palette.m3secondary + ServiceRef { + service: Storage } - Resource { - Layout.bottomMargin: Tokens.padding.large - icon: "hard_disk" - value: SystemUsage.storagePerc - colour: Colours.palette.m3tertiary + RowLayout { + id: layout + + anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.large + + Resource { + id: cpu + + icon: "memory" + value: Math.round(Cpu.percentage * 100) + "%" + fillValue: Cpu.percentage + colour: Colours.palette.m3primary + shapeColour: Colours.palette.m3primaryContainer + fillColour: Qt.alpha(Colours.palette.m3secondary, 0.3) + shape: MaterialShape.Pentagon + + MaterialShape { + x: cpu.mShape.pointAtAngle(45).x - implicitSize / 2 + Tokens.padding.medium + y: cpu.mShape.pointAtAngle(45).y - implicitSize / 2 + + shape: Cpu.temperature > 90 ? MaterialShape.SoftBurst : MaterialShape.Circle + color: Cpu.temperature > 90 ? Colours.palette.m3errorContainer : Colours.palette.m3secondaryContainer + implicitSize: { + const size = Math.round(tempLabel.implicitHeight * 2); + return size % 2 === 0 ? size : size + 1; // Ensure even size so center works properly + } + + Behavior on color { + CAnim {} + } + + StyledText { + id: tempLabel + + anchors.centerIn: parent + anchors.verticalCenterOffset: Math.round(fontInfo.pointSize * 0.04) + + text: { + const temp = Cpu.temperature; + const useF = GlobalConfig.services.useFahrenheitPerformance; + return `${Math.ceil(useF ? temp * 1.8 + 32 : temp)}°${useF ? "F" : "C"}`; + } + color: Cpu.temperature > 90 ? Colours.palette.m3onErrorContainer : Colours.palette.m3secondary + font: Tokens.font.title.builders.medium.scale(cpu.width / 112).width(50).build() + } + } + } + + Resource { + icon: "memory_alt" + value: Math.round(Memory.percentage * 100) + "%" + fillValue: Memory.percentage + colour: Colours.palette.m3tertiary + shapeColour: Colours.palette.m3onTertiary + fillColour: Qt.alpha(Colours.palette.m3tertiary, 0.3) + shape: MaterialShape.Slanted + } + + Resource { + icon: "hard_disk" + value: Math.round(Storage.percentage * 100) + "%" + fillValue: Storage.percentage + colour: Colours.palette.m3secondary + shapeColour: Colours.palette.m3secondaryContainer + fillColour: Qt.alpha(Colours.palette.m3secondary, 0.4) + shape: MaterialShape.Gem + } } - component Resource: StyledRect { + component Resource: Item { id: res required property string icon - required property real value + required property string value required property color colour + required property color shapeColour + property color fillColour + property real fillValue: -1 + property alias shape: shape.shape + readonly property alias mShape: shape Layout.fillWidth: true implicitHeight: width - color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Tokens.rounding.large - - CircularProgress { - id: circ - - anchors.fill: parent - value: res.value - padding: Tokens.padding.large * 3 - fgColour: res.colour - bgColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) - strokeWidth: width < 200 ? Tokens.padding.smaller : Tokens.padding.normal + Behavior on shapeColour { + CAnim {} } - MaterialIcon { - id: icon + MaterialShape { + id: shape - anchors.centerIn: parent - text: res.icon - color: res.colour - font.pointSize: (circ.arcRadius * 0.7) || 1 - font.weight: 600 + implicitSize: res.width + color: Qt.alpha(res.shapeColour, 1) + opacity: res.shapeColour.a + layer.enabled: true } - Behavior on value { - Anim { - type: Anim.StandardLarge + Loader { + id: fillLoader + + anchors.fill: shape + active: res.fillValue >= 0 + asynchronous: true + + layer.enabled: active + layer.effect: Mask { + maskSource: shape } + + sourceComponent: Item { + WavyTopRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + implicitHeight: shape.implicitSize * res.fillValue + color: res.fillColour + } + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: -Tokens.spacing.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: res.icon + color: Colours.palette.m3secondary + fontStyle: Tokens.font.icon.builders.medium.scale(root.fontScale).build() + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: res.value + color: res.colour + font: Tokens.font.headline.builders.large.scale(root.fontScale).width(50).build() + } + } + + Behavior on fillValue { + Anim {} } } } diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml index d96a54df..74ae255c 100644 --- a/modules/lock/WeatherInfo.qml +++ b/modules/lock/WeatherInfo.qml @@ -1,172 +1,23 @@ -pragma ComponentBehavior: Bound - +import "weather" import QtQuick -import QtQuick.Layouts import Caelestia.Config import qs.components import qs.services -ColumnLayout { +StyledRect { id: root required property int rootHeight + readonly property bool showForecast: rootHeight >= Tokens.sizes.lock.showForecastHeight - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Tokens.padding.large * 2 - - spacing: Tokens.spacing.small - - Loader { - asynchronous: true - Layout.topMargin: Tokens.padding.large * 2 - Layout.bottomMargin: -Tokens.padding.large - Layout.alignment: Qt.AlignHCenter - - active: root.rootHeight > 610 - visible: active - - sourceComponent: StyledText { - text: qsTr("Weather") - color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.large - - MaterialIcon { - animate: true - text: Weather.icon - color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.extraLarge * 2.5 - } - - ColumnLayout { - spacing: Tokens.spacing.small - - StyledText { - Layout.fillWidth: true - - animate: true - text: Weather.description - color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.large - font.weight: 500 - elide: Text.ElideRight - } - - StyledText { - Layout.fillWidth: true - - animate: true - text: qsTr("Humidity: %1%").arg(Weather.humidity) - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - } - - Loader { - asynchronous: true - Layout.rightMargin: Tokens.padding.smaller - active: root.width > 400 - visible: active - - sourceComponent: ColumnLayout { - spacing: Tokens.spacing.small - - StyledText { - Layout.fillWidth: true - - animate: true - text: Weather.temp - color: Colours.palette.m3primary - horizontalAlignment: Text.AlignRight - font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 - elide: Text.ElideLeft - } - - StyledText { - Layout.fillWidth: true - - animate: true - text: qsTr("Feels like: %1").arg(Weather.feelsLike) - color: Colours.palette.m3outline - horizontalAlignment: Text.AlignRight - font.pointSize: Tokens.font.size.smaller - elide: Text.ElideLeft - } - } - } - } - - Loader { - id: forecastLoader - - asynchronous: true - Layout.topMargin: Tokens.spacing.smaller - Layout.bottomMargin: Tokens.padding.large * 2 - Layout.fillWidth: true - - active: root.rootHeight > 820 - visible: active - - sourceComponent: RowLayout { - spacing: Tokens.spacing.large - - Repeater { - model: { - const forecast = Weather.hourlyForecast; - const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5; - if (!forecast) - return Array.from({ - length: count - }, () => null); - - return forecast.slice(0, count); - } - - ColumnLayout { - id: forecastHour - - required property var modelData - - Layout.fillWidth: true - spacing: Tokens.spacing.small - - StyledText { - Layout.fillWidth: true - text: { - const hour = forecastHour.modelData?.hour ?? 0; - return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`; - } - color: Colours.palette.m3outline - horizontalAlignment: Text.AlignHCenter - font.pointSize: Tokens.font.size.larger - } - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: forecastHour.modelData?.icon ?? "cloud_alert" - font.pointSize: Tokens.font.size.extraLarge * 1.5 - font.weight: 500 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: GlobalConfig.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` - color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.larger - } - } - } - } + implicitHeight: { + const base = brief.implicitHeight + brief.anchors.topMargin; + if (showForecast) + return base + Tokens.spacing.largeIncreased + forecast.implicitHeight + forecast.anchors.margins; + return base + brief.anchors.topMargin; } + radius: Tokens.rounding.extraExtraLarge + color: Colours.tPalette.m3surfaceContainer Timer { running: true @@ -175,4 +26,28 @@ ColumnLayout { interval: 900000 // 15 minutes onTriggered: Weather.reload() } + + BriefInfo { + id: brief + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Tokens.padding.extraLarge + + rootHeight: root.rootHeight + } + + Loader { + id: forecast + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Tokens.padding.large + + active: root.showForecast + asynchronous: true + + sourceComponent: Forecast {} + } } diff --git a/modules/lock/center/Clock.qml b/modules/lock/center/Clock.qml new file mode 100644 index 00000000..d927d9af --- /dev/null +++ b/modules/lock/center/Clock.qml @@ -0,0 +1,93 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Caelestia.Config +import qs.components +import qs.services + +Item { + id: root + + required property real centerScale + + function calcTopOff(metrics: TextMetrics): real { + return metrics.tightBoundingRect.y - metrics.boundingRect.y; + } + + implicitWidth: hours.implicitWidth + minutes.implicitWidth + Tokens.spacing.small + implicitHeight: hourMetrics.tightBoundingRect.height + + StyledText { + id: hours + + y: -root.calcTopOff(hourMetrics) + text: Time.hourStr + color: Colours.palette.m3primary + font: Tokens.font.headline.builders.large.scale(7 * root.centerScale).width(30).build() + + TextMetrics { + id: hourMetrics + + text: hours.text + font: hours.font + } + } + + StyledText { + id: minutes + + anchors.right: parent.right + y: -root.calcTopOff(minuteMetrics) + + text: Time.minuteStr + color: Colours.palette.m3secondary + font: Tokens.font.headline.builders.large.scale((GlobalConfig.services.useTwelveHourClock ? 3.8 : 7) * root.centerScale).width(30).build() + + TextMetrics { + id: minuteMetrics + + text: minutes.text + font: minutes.font + } + } + + Loader { + anchors.left: minutes.left + anchors.leftMargin: minuteMetrics.tightBoundingRect.x + y: hourMetrics.tightBoundingRect.height - implicitHeight + + active: GlobalConfig.services.useTwelveHourClock + asynchronous: true + + sourceComponent: StyledRect { + color: Colours.tPalette.m3surfaceContainerHigh + radius: Tokens.rounding.large + + implicitWidth: minuteMetrics.tightBoundingRect.width + implicitHeight: amPmMetrics.tightBoundingRect.height + Tokens.padding.large * 2 + + StyledText { + id: amPm + + anchors.centerIn: parent + width: amPmMetrics.tightBoundingRect.width + height: amPmMetrics.tightBoundingRect.height + transform: Translate { + x: -amPmMetrics.tightBoundingRect.x + y: -root.calcTopOff(amPmMetrics) + } + + text: Time.amPmStr + color: Colours.palette.m3onSurface + font: Tokens.font.headline.builders.small.scale(2 * root.centerScale).width(30).build() + + TextMetrics { + id: amPmMetrics + + text: amPm.text + font: amPm.font + } + } + } + } +} diff --git a/modules/lock/InputField.qml b/modules/lock/center/InputField.qml similarity index 88% rename from modules/lock/InputField.qml rename to modules/lock/center/InputField.qml index 1b6c34a7..78ba7bb6 100644 --- a/modules/lock/InputField.qml +++ b/modules/lock/center/InputField.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Layouts import Quickshell import Caelestia.Config import qs.components import qs.services +import qs.modules.lock Item { id: root @@ -14,9 +14,6 @@ Item { readonly property alias placeholder: placeholder property string buffer - Layout.fillWidth: true - Layout.fillHeight: true - clip: true Connections { @@ -49,13 +46,14 @@ Item { animate: true color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - font.family: Tokens.font.family.mono + font: Tokens.font.mono.medium opacity: root.buffer ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -74,10 +72,10 @@ Item { anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0 implicitWidth: fullWidth - implicitHeight: Tokens.font.size.normal + implicitHeight: Tokens.font.body.medium.pointSize orientation: Qt.Horizontal - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall interactive: false model: ScriptModel { @@ -91,7 +89,7 @@ Item { implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface - radius: Tokens.rounding.small / 2 + radius: Tokens.rounding.medium / 2 opacity: 0 scale: 0 @@ -111,6 +109,7 @@ Item { } ParallelAnimation { Anim { + type: Anim.DefaultEffects target: ch property: "opacity" to: 0 @@ -129,7 +128,9 @@ Item { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { diff --git a/modules/lock/center/PasswordInput.qml b/modules/lock/center/PasswordInput.qml new file mode 100644 index 00000000..fb01d53c --- /dev/null +++ b/modules/lock/center/PasswordInput.qml @@ -0,0 +1,123 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +StyledRect { + id: root + + required property int centerWidth + required property var lock + + implicitWidth: centerWidth * 0.8 + implicitHeight: input.implicitHeight + Tokens.padding.small + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.full + + focus: true + onActiveFocusChanged: { + if (!activeFocus) + forceActiveFocus(); + } + + Keys.onPressed: event => { + if (root.lock.unlocking) + return; + + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) + inputField.placeholder.animate = false; + + root.lock.pam.handleKey(event); + } + + StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + onClicked: parent.forceActiveFocus() + } + + RowLayout { + id: input + + anchors.fill: parent + anchors.margins: Tokens.padding.extraSmall + spacing: Tokens.spacing.medium + + Item { + Layout.fillHeight: true + implicitWidth: height + + MaterialIcon { + id: fprintIcon + + anchors.centerIn: parent + animate: true + text: { + if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries) + return "fingerprint_off"; + if (root.lock.pam.fprint.active) + return "fingerprint"; + return "lock"; + } + color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface + opacity: root.lock.pam.passwd.active ? 0 : 1 + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Loader { + anchors.fill: parent + anchors.margins: Tokens.padding.small + + active: opacity > 0 + opacity: root.lock.pam.passwd.active ? 1 : 0 + + sourceComponent: LoadingIndicator {} + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + } + + InputField { + id: inputField + + Layout.fillWidth: true + Layout.fillHeight: true + + pam: root.lock.pam + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: enterIcon.implicitHeight + Tokens.padding.small + + color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + radius: Tokens.rounding.full + + StateLayer { + color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + onClicked: root.lock.pam.passwd.start() + } + + MaterialIcon { + id: enterIcon + + anchors.centerIn: parent + text: "arrow_forward" + color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + fontStyle: Tokens.font.icon.size(Tokens.font.icon.large.pointSize).weight(Font.Medium).build() + } + } + } +} diff --git a/modules/lock/center/ProfilePic.qml b/modules/lock/center/ProfilePic.qml new file mode 100644 index 00000000..dafd7ba8 --- /dev/null +++ b/modules/lock/center/ProfilePic.qml @@ -0,0 +1,56 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import M3Shapes +import Caelestia.Config +import qs.components +import qs.components.effects +import qs.components.images +import qs.services +import qs.utils + +Item { + id: root + + required property int centerWidth + readonly property color bgColour: Colours.tPalette.m3surfaceContainerHighest + + implicitWidth: Math.round(centerWidth * 0.7) + implicitHeight: { + shape.height; // Force update when shape height changes + return shape.pathBounds().height; + } + + MaterialShape { + id: shape + + anchors.centerIn: parent + implicitSize: root.implicitWidth + + shape: MaterialShape.ClamShell + color: Qt.alpha(root.bgColour, 1) + opacity: root.bgColour.a + layer.enabled: true + } + + MaterialIcon { + anchors.centerIn: parent + + text: "person" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.size(root.centerWidth / 4).build() + visible: pfp.status !== Image.Ready + } + + CachingImage { + id: pfp + + anchors.fill: shape + path: `${Paths.home}/.face` + + layer.enabled: true + layer.effect: Mask { + maskSource: shape + } + } +} diff --git a/modules/lock/center/StateMessage.qml b/modules/lock/center/StateMessage.qml new file mode 100644 index 00000000..40b09643 --- /dev/null +++ b/modules/lock/center/StateMessage.qml @@ -0,0 +1,211 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Caelestia.Config +import qs.components +import qs.services +import qs.modules.lock + +Item { + id: root + + required property Pam pam + + readonly property string msg: { + if (pam.fprintState === "error") + return qsTr("FP ERROR: %1").arg(pam.fprint.message); + if (pam.state === "error") + return qsTr("PW ERROR: %1").arg(pam.passwd.message); + + if (pam.lockMessage) + return pam.lockMessage; + + if (pam.state === "max" && pam.fprintState === "max") + return qsTr("Maximum password and fingerprint attempts reached."); + if (pam.state === "max") { + if (pam.fprint.available) + return qsTr("Maximum password attempts reached. Please use fingerprint."); + return qsTr("Maximum password attempts reached."); + } + if (pam.fprintState === "max") + return qsTr("Maximum fingerprint attempts reached. Please use password."); + + if (pam.state === "fail") { + if (pam.fprint.available) + return qsTr("Incorrect password. Please try again or use fingerprint."); + return qsTr("Incorrect password. Please try again."); + } + if (pam.fprintState === "fail") + return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries); + + return ""; + } + + readonly property string stateMsg: { + if (Hypr.kbLayout !== Hypr.defaultKbLayout) { + if (Hypr.capsLock && Hypr.numLock) + return qsTr("Caps lock and Num lock are ON.\nKeyboard layout: %1").arg(Hypr.kbLayoutFull); + if (Hypr.capsLock) + return qsTr("Caps lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); + if (Hypr.numLock) + return qsTr("Num lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); + return qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull); + } + + if (Hypr.capsLock && Hypr.numLock) + return qsTr("Caps lock and Num lock are ON."); + if (Hypr.capsLock) + return qsTr("Caps lock is ON."); + if (Hypr.numLock) + return qsTr("Num lock is ON."); + + return ""; + } + + property bool stateMsgShouldBeVisible + + onMsgChanged: { + if (msg) { + if (message.opacity > 0) { + message.animate = true; + message.text = msg; + message.animate = false; + + exitAnim.stop(); + if (message.scale < 1) + appearAnim.restart(); + else + flashAnim.restart(); + } else { + message.text = msg; + exitAnim.stop(); + appearAnim.restart(); + } + } else { + appearAnim.stop(); + flashAnim.stop(); + exitAnim.start(); + } + } + + onStateMsgChanged: { + if (stateMsg) { + if (stateMessage.opacity > 0) { + stateMessage.animate = true; + stateMessage.text = stateMsg; + stateMessage.animate = false; + } else { + stateMessage.text = stateMsg; + } + stateMsgShouldBeVisible = true; + } else { + stateMsgShouldBeVisible = false; + } + } + + implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) + + Behavior on implicitHeight { + Anim {} + } + + StyledText { + id: stateMessage + + anchors.left: parent.left + anchors.right: parent.right + + scale: root.stateMsgShouldBeVisible && !root.msg ? 1 : 0.7 + opacity: root.stateMsgShouldBeVisible && !root.msg ? 1 : 0 + color: Colours.palette.m3onSurfaceVariant + + font: Tokens.font.mono.small + horizontalAlignment: Qt.AlignHCenter + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + lineHeight: 1.2 + + Behavior on scale { + Anim {} + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + StyledText { + id: message + + anchors.left: parent.left + anchors.right: parent.right + + scale: 0.7 + opacity: 0 + color: Colours.palette.m3error + + font: Tokens.font.mono.small + horizontalAlignment: Qt.AlignHCenter + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + + Connections { + function onFlashMsg(): void { + exitAnim.stop(); + if (message.scale < 1) + appearAnim.restart(); + else + flashAnim.restart(); + } + + target: root.pam + } + + Anim { + id: appearAnim + + type: Anim.DefaultEffects + target: message + properties: "scale,opacity" + to: 1 + onFinished: flashAnim.restart() + } + + SequentialAnimation { + id: flashAnim + + loops: 2 + + FlashAnim { + to: 0.3 + } + FlashAnim { + to: 1 + } + } + + ParallelAnimation { + id: exitAnim + + Anim { + target: message + property: "scale" + to: 0.7 + type: Anim.StandardLarge + } + Anim { + target: message + property: "opacity" + to: 0 + type: Anim.StandardLarge + } + } + } + + component FlashAnim: NumberAnimation { + target: message + property: "opacity" + duration: Tokens.anim.durations.small + easing.type: Easing.Linear + } +} diff --git a/modules/lock/weather/BriefInfo.qml b/modules/lock/weather/BriefInfo.qml new file mode 100644 index 00000000..7d87dfc1 --- /dev/null +++ b/modules/lock/weather/BriefInfo.qml @@ -0,0 +1,63 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services + +ColumnLayout { + id: root + + required property int rootHeight + + spacing: Tokens.spacing.extraSmall + + StyledText { + Layout.alignment: Qt.AlignHCenter + animate: true + text: Weather.description + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.large + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Tokens.spacing.medium + + StyledText { + id: temp + + animate: true + text: Weather.temp + color: Colours.palette.m3primary + font: Tokens.font.headline.builders.large.scale(1.5).weight(Font.DemiBold).width(80).build() + } + + MaterialIcon { + animate: true + text: Weather.icon + color: Colours.palette.m3secondary + fontStyle: Tokens.font.headline.builders.large.scale(1.5).build() + } + } + + StyledText { + visible: root.rootHeight > Tokens.sizes.lock.showWeatherDetailsHeight + Layout.alignment: Qt.AlignHCenter + animate: true + text: qsTr("Feels like %1").arg(Weather.temp) + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.large + } + + StyledText { + visible: root.rootHeight > Tokens.sizes.lock.showWeatherDetailsHeight + Layout.alignment: Qt.AlignHCenter + animate: true + text: { + const today = Weather.forecast[0]; + return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC)); + } + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.medium + } +} diff --git a/modules/lock/weather/Forecast.qml b/modules/lock/weather/Forecast.qml new file mode 100644 index 00000000..d23d357e --- /dev/null +++ b/modules/lock/weather/Forecast.qml @@ -0,0 +1,105 @@ +import QtQuick +import QtQuick.Layouts +import M3Shapes +import Caelestia +import Caelestia.Config +import qs.components +import qs.services + +StyledRect { + id: root + + color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + radius: Tokens.rounding.extraLargeIncreased + implicitHeight: header.anchors.margins + header.implicitHeight + Tokens.spacing.medium + layout.implicitHeight + layout.anchors.bottomMargin + + RowLayout { + id: header + + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Tokens.padding.largeIncreased + + spacing: Tokens.spacing.small + + MaterialIcon { + Layout.topMargin: Math.round(fontInfo.pointSize * 0.12) + text: "schedule" + fontStyle: Tokens.font.icon.builders.medium.weight(title.font.weight).build() + } + + StyledText { + id: title + + text: qsTr("Hourly forecast") + font: Tokens.font.title.medium + } + } + + RowLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: Tokens.padding.largeIncreased + anchors.margins: Tokens.padding.large + + spacing: Tokens.spacing.small + + Repeater { + model: CUtils.clamp(Math.floor((layout.width + layout.spacing) / (Tokens.sizes.lock.forecastItemWidth + layout.spacing)), 0, Weather.hourlyForecast.length) + + ColumnLayout { + id: hour + + required property int index + readonly property var cond: Weather.hourlyForecast[index] + + Layout.fillWidth: true + spacing: Tokens.spacing.extraSmall + + MaterialShape { + Layout.alignment: Qt.AlignHCenter + implicitSize: temp.implicitHeight + Tokens.padding.medium * 2 + shape: MaterialShape.Cookie4Sided + color: Qt.alpha(Colours.palette.m3primary, hour.index === 0 ? 1 : 0) + + Behavior on color { + CAnim {} + } + + StyledText { + id: temp + + anchors.centerIn: parent + text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F + color: hour.index === 0 ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + font: Tokens.font.title.medium + } + } + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: hour.cond.icon + color: Colours.palette.m3secondary + fontStyle: Tokens.font.icon.large + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: hour.cond.precipChance + "%" + color: Colours.palette.m3primary + } + + StyledText { + Layout.topMargin: Tokens.spacing.extraSmall + Layout.alignment: Qt.AlignHCenter + text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), GlobalConfig.services.useTwelveHourClock ? "ha" : "hh00") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.medium + } + } + } + } +} diff --git a/modules/nexus/NavPane.qml b/modules/nexus/NavPane.qml new file mode 100644 index 00000000..a5fb94e8 --- /dev/null +++ b/modules/nexus/NavPane.qml @@ -0,0 +1,26 @@ +import "navpane" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.modules.nexus + +ColumnLayout { + id: root + + required property NexusState nState + + spacing: Tokens.spacing.large + + SearchBar { + Layout.fillWidth: true + nState: root.nState + } + + NavLocations { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: -topMargin + Layout.bottomMargin: -bottomMargin + nState: root.nState + } +} diff --git a/modules/nexus/Nexus.qml b/modules/nexus/Nexus.qml new file mode 100644 index 00000000..90035bc3 --- /dev/null +++ b/modules/nexus/Nexus.qml @@ -0,0 +1,109 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Caelestia.Blobs +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus + +Item { + id: root + + readonly property NexusState nState: NexusState { + id: nState + + onClose: root.close() + } + property color blobColour: Colours.tPalette.m3surfaceContainerLow + + signal close + + implicitWidth: implicitHeight * Tokens.sizes.nexus.ratio + implicitHeight: nState.screen.height * Tokens.sizes.nexus.heightMult + + Behavior on blobColour { + CAnim {} + } + + BlobGroup { + id: blobGroup + + smoothing: root.Tokens.rounding.medium + color: root.blobColour + } + + BlobInvertedRect { + anchors.fill: parent + group: blobGroup + opacity: root.blobColour.a + radius: Tokens.rounding.large + + borderLeft: navPane.width + navPane.anchors.margins * 2 + borderRight: Tokens.padding.medium + borderTop: Tokens.padding.medium + borderBottom: Tokens.padding.medium + } + + BlobRect { + id: windowBtnRect + + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: root.nState.isWindow ? 0 : Tokens.padding.extraSmall + + group: blobGroup + opacity: root.blobColour.a + radius: Tokens.rounding.medium + + implicitWidth: windowBtn.implicitWidth + (root.nState.isWindow ? Tokens.padding.extraSmall : Tokens.padding.small) * 2 + implicitHeight: windowBtn.implicitHeight + (root.nState.isWindow ? Tokens.padding.extraSmall : Tokens.padding.small) + } + + IconButton { + id: windowBtn + + anchors.centerIn: windowBtnRect + icon: nState.isWindow ? "close" : "pip" + type: IconButton.Text + label.fill: 0 + inactiveOnColour: hovered ? nState.isWindow ? Colours.palette.m3error : Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + stateLayer.opacity: 0 + onClicked: { + if (!nState.isWindow) + WindowFactory.create(); + root.close(); + } + + label.scale: pressed ? 0.8 : 1 + label.renderType: Text.QtRendering + + Behavior on label.scale { + Anim {} + } + } + + NavPane { + id: navPane + + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: Tokens.padding.large + + nState: nState + width: Math.min(Tokens.sizes.nexus.maxNavWidth, Math.round(root.width / 3)) + } + + Pages { + anchors.left: navPane.right + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.leftMargin: navPane.anchors.margins + anchors.margins + anchors.margins: Tokens.padding.extraLarge + + nState: nState + } +} diff --git a/modules/nexus/NexusState.qml b/modules/nexus/NexusState.qml new file mode 100644 index 00000000..818f35c0 --- /dev/null +++ b/modules/nexus/NexusState.qml @@ -0,0 +1,30 @@ +import QtQuick +import Quickshell +import Quickshell.Bluetooth + +QtObject { + property ShellScreen screen + property bool isWindow + property int currentPageIdx + property list subPageIdxStack + property bool searchOpen + + property string selectedWallpaperCategory + property BluetoothDevice selectedBtDevice + + signal close + signal subPageOpened(idx: int) + signal subPageClosed + + function openSubPage(idx: int): void { + subPageIdxStack.push(idx); + subPageOpened(idx); + } + + function closeSubPage(): void { + subPageClosed(); + subPageIdxStack.pop(); + } + + onCurrentPageIdxChanged: subPageIdxStack.length = 0 +} diff --git a/modules/nexus/PageCompRegistry.qml b/modules/nexus/PageCompRegistry.qml new file mode 100644 index 00000000..8c48110b --- /dev/null +++ b/modules/nexus/PageCompRegistry.qml @@ -0,0 +1,180 @@ +pragma Singleton + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services +import qs.modules.nexus.common +import qs.modules.nexus.pages +import qs.modules.nexus.pages.audio +import qs.modules.nexus.pages.bluetooth +import qs.modules.nexus.pages.panels +import qs.modules.nexus.pages.wallandstyle +import qs.modules.nexus.pages.panels.taskbar + +QtObject { + id: root + + readonly property list pageComps: [ + // Appearance + Component { + // Wallpaper & style + StackPage { + Component { + WallpaperAndStyle {} + } + Component { + WallpaperSelect {} + } + Component { + WallpaperCategory {} + } + Component { + ColourSelect {} + } + } + }, + + // Connectivity + Component { + // Network + StackPage { + Component { + NetworkPage {} + } + } + }, + Component { + // Bluetooth + StackPage { + Component { + BluetoothPage {} + } + Component { + BtDeviceInfo {} + } + Component { + BluetoothPairing {} + } + } + }, + Component { + // Audio + StackPage { + Component { + AudioPage {} + } + Component { + AppVolumes {} + } + } + }, + + // System + Component { + PlaceholderComp {} + }, + Component { + PlaceholderComp {} + }, + + // Shell + Component { + // Panels + StackPage { + Component { + PanelsPage {} + } + Component { + DashboardPanel {} + } + Component { + TaskbarPanel {} + } + Component { + LauncherPanel {} + } + Component { + SidebarPanel {} + } + + // Taskbar component sub-pages + Component { + BarWorkspaces {} + } + Component { + BarActiveWindow {} + } + Component { + BarTray {} + } + Component { + BarStatusIcons {} + } + Component { + BarClock {} + } + } + }, + Component { + // Services + StackPage { + Component { + ServicesPage {} + } + } + }, + Component { + // Language & region + StackPage { + Component { + LanguageAndRegion {} + } + } + }, + + // About + Component { + StackPage { + Component { + AboutPage {} + } + } + } + ] + + readonly property Component placeholderComp: Component { + PlaceholderComp {} + } + + component PlaceholderComp: Item { + property NexusState nState // To avoid the warning from non-existent property + + ColumnLayout { + anchors.centerIn: parent + spacing: Tokens.padding.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "handyman" + color: Colours.palette.m3outlineVariant + font: Tokens.font.icon.extraLarge + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Page under construction") + color: Colours.palette.m3outlineVariant + font: Tokens.font.title.large + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("This page will be available in a future update.") + color: Colours.palette.m3outlineVariant + font: Tokens.font.body.large + } + } + } +} diff --git a/modules/nexus/PageRegistry.qml b/modules/nexus/PageRegistry.qml new file mode 100644 index 00000000..ea3ad71e --- /dev/null +++ b/modules/nexus/PageRegistry.qml @@ -0,0 +1,87 @@ +pragma Singleton + +import QtQuick + +QtObject { + id: root + + readonly property list pages: [ + // Appearance + { + label: qsTr("Wallpaper & style"), + icon: "palette", + description: qsTr("Wallpaper, fonts, colours"), + category: "appearance" + }, + + // Connectivity + // TODO + // { + // label: qsTr("Display"), + // icon: "monitor", + // description: qsTr("Output configuration"), + // category: "connectivity" + // }, + { + label: qsTr("Network"), + icon: "wifi", + description: qsTr("Wi-Fi, ethernet"), + category: "connectivity" + }, + { + label: qsTr("Connected devices"), + icon: "devices_other", + description: qsTr("Bluetooth, pairing"), + category: "connectivity", + noFill: true + }, + { + label: qsTr("Audio"), + icon: "volume_up", + description: qsTr("App volumes, sound devices"), + category: "connectivity" + }, + + // System + { + label: qsTr("Updates"), + icon: "update", + description: qsTr("System updates"), + category: "system" + }, + { + label: qsTr("Plugins"), + icon: "extension", + description: qsTr("Manage plugins"), + category: "system" + }, + + // Shell + { + label: qsTr("Panels"), + icon: "dock_to_bottom", + description: qsTr("Dashboard, taskbar, launcher, sidebar"), + category: "shell" + }, + { + label: qsTr("Services"), + icon: "build", + description: qsTr("Poll intervals, lyrics backend"), + category: "shell" + }, + { + label: qsTr("Language & region"), + icon: "globe", + description: qsTr("UI language, weather location, display units"), + category: "shell" + }, + + // About + { + label: qsTr("About"), + icon: "info", + description: qsTr("System information, credits"), + category: "about" + }, + ] +} diff --git a/modules/nexus/Pages.qml b/modules/nexus/Pages.qml new file mode 100644 index 00000000..655cd9cf --- /dev/null +++ b/modules/nexus/Pages.qml @@ -0,0 +1,95 @@ +import QtQuick +import Caelestia.Config +import qs.components +import qs.modules.nexus + +Item { + id: root + + required property NexusState nState + + property int lastPageIdx + property int animOff + property Item currentItem + + function loadPage(idx: int): void { + if (currentItem) + currentItem.destroy(); + + const comp = PageCompRegistry.pageComps[idx] ?? PageCompRegistry.placeholderComp; + const incubator = comp.incubateObject(container, { + nState + }); + + const attach = () => { + incubator.object.anchors.fill = container; + currentItem = incubator.object; + }; + + if (incubator.status === Component.Ready) + attach(); + else + incubator.onStatusChanged = status => { + if (status === Component.Ready) + attach(); + }; + } + + Item { + id: container + + anchors.fill: parent + layer.enabled: opacity < 1 + Component.onCompleted: root.loadPage(root.nState.currentPageIdx) + } + + Connections { + function onCurrentPageIdxChanged(): void { + switchAnim.complete(); + root.animOff = root.Tokens.padding.extraLarge * (root.nState.currentPageIdx > root.lastPageIdx ? 1 : -1); + switchAnim.start(); + root.lastPageIdx = root.nState.currentPageIdx; + } + + target: root.nState + } + + SequentialAnimation { + id: switchAnim + + Anim { + target: container + property: "opacity" + to: 0 + type: Anim.DefaultEffects + } + ScriptAction { + script: root.loadPage(root.nState.currentPageIdx) + } + PropertyAction { + target: container.anchors + property: "topMargin" + value: root.animOff + } + PropertyAction { + target: container.anchors + property: "bottomMargin" + value: -root.animOff + } + ParallelAnimation { + Anim { + target: container + property: "opacity" + from: 0 + to: 1 + type: Anim.SlowEffects + } + Anim { + target: container.anchors + properties: "topMargin,bottomMargin" + to: 0 + type: Anim.SlowEffects + } + } + } +} diff --git a/modules/nexus/WindowFactory.qml b/modules/nexus/WindowFactory.qml new file mode 100644 index 00000000..ab26d15f --- /dev/null +++ b/modules/nexus/WindowFactory.qml @@ -0,0 +1,60 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Caelestia.Config +import qs.components +import qs.services +import qs.modules.nexus + +Singleton { + id: root + + function create(parent: Item, props: var): void { + nexusComp.createObject(parent ?? dummy, props); + } + + QtObject { + id: dummy + } + + Component { + id: nexusComp + + FloatingWindow { + id: win + + color: Colours.tPalette.m3surface + surfaceFormat.opaque: false + + onVisibleChanged: { + if (!visible) + destroy(); + } + + implicitWidth: nexus.implicitWidth + implicitHeight: nexus.implicitHeight + + minimumSize.width: contentItem.Tokens.sizes.nexus.minWidth + minimumSize.height: contentItem.Tokens.sizes.nexus.minHeight + + contentItem.Config.screen: screen.name + contentItem.Tokens.screen: screen.name + + title: qsTr("Nexus — %1").arg(PageRegistry.pages[nexus.nState.currentPageIdx].label) + + Nexus { + id: nexus + + anchors.fill: parent + nState.screen: win.screen + nState.isWindow: true + onClose: win.destroy() + } + + Behavior on color { + CAnim {} + } + } + } +} diff --git a/modules/nexus/common/AnimatedLogo.qml b/modules/nexus/common/AnimatedLogo.qml new file mode 100644 index 00000000..6d2aaa00 --- /dev/null +++ b/modules/nexus/common/AnimatedLogo.qml @@ -0,0 +1,513 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import QtQuick.Shapes +import qs.components +import qs.services + +Item { + id: root + + property real blurAmount: skipIntroAnimation ? 0.0 : 1.0 + property bool skipIntroAnimation: false + + property real star1Opacity: skipIntroAnimation ? 1.0 : 0.0 + property real star2Opacity: skipIntroAnimation ? 1.0 : 0.0 + property real star3Opacity: skipIntroAnimation ? 1.0 : 0.0 + + property real star1Scale: skipIntroAnimation ? 1.0 : 0.0 + property real star2Scale: skipIntroAnimation ? 1.0 : 0.0 + property real star3Scale: skipIntroAnimation ? 1.0 : 0.0 + + readonly property alias topShape: topShape + readonly property alias bottomShape: bottomShape + readonly property alias star1: star1 + readonly property alias star2: star2 + readonly property alias star3: star3 + + signal animationCompleted + + implicitWidth: 128 + implicitHeight: 90.38 + + Item { + id: logo + + readonly property real designWidth: 128 + readonly property real designHeight: 90.38 + + property color topColour: Colours.palette.m3primary + property color bottomColour: Colours.palette.m3onSurface + + implicitWidth: designWidth + implicitHeight: designHeight + + transformOrigin: Item.Center + scale: root.skipIntroAnimation ? 1.0 : 0.0 + opacity: root.skipIntroAnimation ? 1.0 : 0.0 + rotation: 0.0 + + layer.enabled: root.blurAmount > 0 + layer.effect: MultiEffect { + blurEnabled: true + blur: root.blurAmount + blurMax: 60 + } + + Component.onCompleted: { + root.star1.opacity = Qt.binding(() => root.star1Opacity); + root.star1.scale = Qt.binding(() => root.star1Scale); + root.star2.opacity = Qt.binding(() => root.star2Opacity); + root.star2.scale = Qt.binding(() => root.star2Scale); + root.star3.opacity = Qt.binding(() => root.star3Opacity); + root.star3.scale = Qt.binding(() => root.star3Scale); + } + + Behavior on topColour { + CAnim {} + } + + Behavior on bottomColour { + CAnim {} + } + + Shape { + id: topShape + + anchors.centerIn: parent + width: logo.designWidth + height: logo.designHeight + scale: Math.min(logo.width / width, logo.height / height) + transformOrigin: Item.Center + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: logo.topColour + strokeColor: "transparent" + + PathSvg { + path: "m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z" + } + } + } + + Shape { + id: bottomShape + + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: logo.bottomColour + strokeColor: "transparent" + + PathSvg { + path: "m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z" + } + } + } + + Shape { + id: star1 + + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + opacity: 0.0 + + ShapePath { + fillColor: logo.topColour + strokeColor: "transparent" + + PathSvg { + path: "m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z" + } + } + } + + Shape { + id: star2 + + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + opacity: 0.0 + + ShapePath { + fillColor: logo.topColour + strokeColor: "transparent" + + PathSvg { + path: "m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z" + } + } + } + + Shape { + id: star3 + + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + opacity: 0.0 + + ShapePath { + fillColor: logo.topColour + strokeColor: "transparent" + + PathSvg { + path: "m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z" + } + } + } + } + + SequentialAnimation { + running: !root.skipIntroAnimation + onFinished: root.animationCompleted() + + ParallelAnimation { + SequentialAnimation { + NumberAnimation { + target: logo + property: "rotation" + from: 0 + to: 750 + duration: 1000 + easing.type: Easing.OutCubic + } + + NumberAnimation { + target: logo + property: "rotation" + from: 750 + to: 710 + duration: 300 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: logo + property: "rotation" + from: 710 + to: 725 + duration: 350 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: logo + property: "rotation" + from: 725 + to: 720 + duration: 250 + easing.type: Easing.OutQuad + } + + ScriptAction { + script: logo.rotation = 0 + } + } + + SequentialAnimation { + NumberAnimation { + target: logo + property: "scale" + from: 0.0 + to: 1.08 + duration: 1000 + easing.type: Easing.OutCubic + } + + NumberAnimation { + target: logo + property: "scale" + from: 1.08 + to: 0.96 + duration: 200 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: logo + property: "scale" + from: 0.96 + to: 1.0 + duration: 250 + easing.type: Easing.OutBack + easing.overshoot: 1.05 + } + } + + NumberAnimation { + target: logo + property: "opacity" + from: 0.0 + to: 1.0 + duration: 600 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root + property: "blurAmount" + from: 1.0 + to: 0.0 + duration: 900 + easing.type: Easing.OutCubic + } + + SequentialAnimation { + PauseAnimation { + duration: 1100 + } + + ParallelAnimation { + NumberAnimation { + target: root + property: "star1Opacity" + from: 0.0 + to: 1.0 + duration: 700 + easing.type: Easing.InOutQuad + } + + SequentialAnimation { + NumberAnimation { + target: root + property: "star1Scale" + from: 0.0 + to: 1.08 + duration: 500 + easing.type: Easing.OutQuad + } + + NumberAnimation { + target: root + property: "star1Scale" + from: 1.08 + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + } + } + } + + SequentialAnimation { + PauseAnimation { + duration: 1250 + } + + ParallelAnimation { + NumberAnimation { + target: root + property: "star2Opacity" + from: 0.0 + to: 1.0 + duration: 700 + easing.type: Easing.InOutQuad + } + + SequentialAnimation { + NumberAnimation { + target: root + property: "star2Scale" + from: 0.0 + to: 1.08 + duration: 500 + easing.type: Easing.OutQuad + } + + NumberAnimation { + target: root + property: "star2Scale" + from: 1.08 + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + } + } + } + + SequentialAnimation { + PauseAnimation { + duration: 1400 + } + + ParallelAnimation { + NumberAnimation { + target: root + property: "star3Opacity" + from: 0.0 + to: 1.0 + duration: 700 + easing.type: Easing.InOutQuad + } + + SequentialAnimation { + NumberAnimation { + target: root + property: "star3Scale" + from: 0.0 + to: 1.08 + duration: 500 + easing.type: Easing.OutQuad + } + + NumberAnimation { + target: root + property: "star3Scale" + from: 1.08 + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + } + } + } + } + } + + SequentialAnimation { + running: true + loops: Animation.Infinite + + PauseAnimation { + duration: 2500 + } + + ParallelAnimation { + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star1 + property: "y" + from: root.star1.y + to: root.star1.y - 5 + duration: 2500 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star1 + property: "y" + from: root.star1.y - 5 + to: root.star1.y + duration: 2500 + easing.type: Easing.InOutQuad + } + } + + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star2 + property: "y" + from: root.star2.y + to: root.star2.y + 5 + duration: 3000 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star2 + property: "y" + from: root.star2.y + 5 + to: root.star2.y + duration: 3000 + easing.type: Easing.InOutQuad + } + } + + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star3 + property: "y" + from: root.star3.y + to: root.star3.y - 5 + duration: 2800 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star3 + property: "y" + from: root.star3.y - 5 + to: root.star3.y + duration: 2800 + easing.type: Easing.InOutQuad + } + } + + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star1 + property: "scale" + from: 1.0 + to: 1.08 + duration: 2500 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star1 + property: "scale" + from: 1.08 + to: 1.0 + duration: 2500 + easing.type: Easing.InOutQuad + } + } + + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star2 + property: "scale" + from: 1.0 + to: 1.12 + duration: 3000 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star2 + property: "scale" + from: 1.12 + to: 1.0 + duration: 3000 + easing.type: Easing.InOutQuad + } + } + + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star3 + property: "scale" + from: 1.0 + to: 1.08 + duration: 2800 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star3 + property: "scale" + from: 1.08 + to: 1.0 + duration: 2800 + easing.type: Easing.InOutQuad + } + } + } + } +} diff --git a/modules/nexus/common/AudioDeviceList.qml b/modules/nexus/common/AudioDeviceList.qml new file mode 100644 index 00000000..8aacb579 --- /dev/null +++ b/modules/nexus/common/AudioDeviceList.qml @@ -0,0 +1,97 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire +import Caelestia.Config +import qs.components +import qs.services +import qs.modules.nexus.common + +ItemList { + id: root + + property var nodes: [] + property int currentId: -1 + property string iconName: "speaker" + + signal selected(node: PwNode) + + last: true + showList: true + + model: ScriptModel { + values: [...root.nodes].sort((a, b) => (a.description || a.name || "").localeCompare(b.description || b.name || "")) + } + + delegate: Item { + id: device + + required property PwNode modelData + required property int index + readonly property bool active: device.modelData?.id === root.currentId + + anchors.left: root.list.contentItem.left + anchors.right: root.list.contentItem.right + implicitHeight: deviceLayout.implicitHeight + deviceLayout.anchors.margins * 2 + + StateLayer { + radius: Tokens.rounding.extraSmall + bottomLeftRadius: device.index === root?.list.count - 1 ? Tokens.rounding.extraLarge : radius + bottomRightRadius: device.index === root?.list.count - 1 ? Tokens.rounding.extraLarge : radius + onClicked: root.selected(device.modelData) + } + + RowLayout { + id: deviceLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: devIcon.implicitHeight + Tokens.padding.small * 2 + radius: Tokens.rounding.full + color: device.active ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer + + MaterialIcon { + id: devIcon + + anchors.centerIn: parent + text: root.iconName + color: device.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer + font: Tokens.font.icon.medium + fill: device.active ? 1 : 0 + + Behavior on fill { + Anim {} + } + } + } + + StyledText { + Layout.fillWidth: true + text: device.modelData?.description || device.modelData?.name || qsTr("Unknown") + font: Tokens.font.body.small + elide: Text.ElideRight + } + + MaterialIcon { + text: "check" + color: Colours.palette.m3primary + font: Tokens.font.icon.medium + opacity: device.active ? 1 : 0 + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + } + } +} diff --git a/modules/nexus/common/ConnectedRect.qml b/modules/nexus/common/ConnectedRect.qml new file mode 100644 index 00000000..df5b21ae --- /dev/null +++ b/modules/nexus/common/ConnectedRect.qml @@ -0,0 +1,15 @@ +import QtQuick +import Caelestia.Config +import qs.components +import qs.services + +StyledRect { + property bool first + property bool last + + color: Colours.tPalette.m3surfaceContainer + topLeftRadius: first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + topRightRadius: first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + bottomLeftRadius: last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + bottomRightRadius: last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall +} diff --git a/modules/nexus/common/InfoRow.qml b/modules/nexus/common/InfoRow.qml new file mode 100644 index 00000000..f06094f8 --- /dev/null +++ b/modules/nexus/common/InfoRow.qml @@ -0,0 +1,59 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services +import qs.modules.nexus.common + +ConnectedRect { + id: root + + property alias label: label.text + property string subtext + property alias value: value.text + + Layout.fillWidth: true + implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins * 2 + + RowLayout { + id: rowLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + id: label + + Layout.fillWidth: true + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + visible: root.subtext + text: root.subtext + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + } + } + + StyledText { + id: value + + Layout.maximumWidth: root.width / 2 + horizontalAlignment: Text.AlignRight + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.small + elide: Text.ElideRight + } + } +} diff --git a/modules/nexus/common/ItemList.qml b/modules/nexus/common/ItemList.qml new file mode 100644 index 00000000..debfdcef --- /dev/null +++ b/modules/nexus/common/ItemList.qml @@ -0,0 +1,121 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services + +ConnectedRect { + id: root + + property bool showList + property string placeholderIcon + property string placeholderText + property int extraHeight + + property alias model: list.model + property alias delegate: list.delegate + readonly property alias list: list + + Layout.fillWidth: true + implicitHeight: (showList && list.count > 0 ? list.contentHeight : placeholder.implicitHeight + Tokens.padding.extraLarge * 2) + extraHeight + color: Colours.tPalette.m3surfaceContainer + clip: true + + Behavior on implicitHeight { + Anim {} + } + + Loader { + id: placeholder + + anchors.centerIn: parent + active: opacity > 0 + opacity: root.showList && list.count > 0 ? 0 : 1 + + sourceComponent: ColumnLayout { + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: root.placeholderIcon + color: Colours.palette.m3outline + font: Tokens.font.icon.large + animate: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: root.placeholderText + color: Colours.palette.m3outline + font: Tokens.font.body.large + animate: true + } + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + ListView { + id: list + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + + spacing: 0 + interactive: false + opacity: root.showList ? 1 : 0 + + add: Transition { + Anim { + property: "opacity" + from: 0 + to: 1 + type: Anim.DefaultEffects + } + } + + remove: Transition { + Anim { + property: "opacity" + to: 0 + type: Anim.DefaultEffects + } + } + + move: Transition { + Anim { + property: "opacity" + to: 1 + type: Anim.DefaultEffects + } + Anim { + property: "y" + } + } + + displaced: Transition { + Anim { + property: "opacity" + to: 1 + type: Anim.DefaultEffects + } + Anim { + property: "y" + } + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } +} diff --git a/modules/nexus/common/NavRow.qml b/modules/nexus/common/NavRow.qml new file mode 100644 index 00000000..f8f06eff --- /dev/null +++ b/modules/nexus/common/NavRow.qml @@ -0,0 +1,72 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services +import qs.modules.nexus.common + +ConnectedRect { + id: root + + property alias icon: icon.text + property alias label: label.text + property alias status: status.text + + signal clicked + + Layout.fillWidth: true + implicitHeight: navLayout.implicitHeight + navLayout.anchors.margins * 2 + + StateLayer { + onClicked: root.clicked() + } + + RowLayout { + id: navLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + MaterialIcon { + id: icon + + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.icon.medium + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + id: label + + Layout.fillWidth: true + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + id: status + + Layout.fillWidth: true + visible: text + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + animate: true + } + } + + MaterialIcon { + text: "chevron_right" + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.icon.medium + } + } +} diff --git a/modules/nexus/common/PageBase.qml b/modules/nexus/common/PageBase.qml new file mode 100644 index 00000000..82df2215 --- /dev/null +++ b/modules/nexus/common/PageBase.qml @@ -0,0 +1,61 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.containers +import qs.components.controls +import qs.services +import qs.modules.nexus + +ColumnLayout { + id: root + + required property string title + required property NexusState nState + property bool isSubPage + readonly property int cappedWidth: Math.min(800, width) + + default property Item contentChild + + spacing: Tokens.spacing.extraLargeIncreased + + RowLayout { + spacing: Tokens.spacing.largeIncreased + + Loader { + visible: active + active: root.isSubPage + asynchronous: true + sourceComponent: IconButton { + icon: "arrow_back" + font: Tokens.font.icon.medium + type: IconButton.Tonal + isRound: true + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurfaceVariant + onClicked: root.nState.closeSubPage() + } + } + + StyledText { + text: root.title + font: Tokens.font.title.large + } + } + + VerticalFadeFlickable { + id: flickable + + Layout.fillWidth: true + Layout.fillHeight: true + + Layout.topMargin: -topMargin + topMargin: Tokens.padding.large + bottomMargin: Tokens.padding.extraLarge + + contentHeight: root.contentChild?.implicitHeight ?? 0 + contentItem.children: [root.contentChild] + } +} diff --git a/modules/nexus/common/SectionHeader.qml b/modules/nexus/common/SectionHeader.qml new file mode 100644 index 00000000..442990d5 --- /dev/null +++ b/modules/nexus/common/SectionHeader.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services + +StyledText { + property bool first + + Layout.fillWidth: true + Layout.topMargin: first ? 0 : Tokens.spacing.largeIncreased - ((parent as ColumnLayout).spacing ?? 0) + Layout.bottomMargin: Tokens.spacing.extraSmall + Layout.leftMargin: Tokens.padding.small + + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.label.medium + elide: Text.ElideRight +} diff --git a/modules/nexus/common/SelectRow.qml b/modules/nexus/common/SelectRow.qml new file mode 100644 index 00000000..2c8b00da --- /dev/null +++ b/modules/nexus/common/SelectRow.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus.common + +ConnectedRect { + id: root + + property alias label: label.text + property string subtext + property alias menuItems: splitButton.menuItems + property alias active: splitButton.active + property alias fallbackText: splitButton.fallbackText + property alias fallbackIcon: splitButton.fallbackIcon + property alias menuOnTop: splitButton.menuOnTop + + signal selected(item: MenuItem) + + implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins * 2 + clip: false + z: splitButton.expanded ? 1 : 0 + + RowLayout { + id: rowLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + id: label + + Layout.fillWidth: true + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + visible: root.subtext + text: root.subtext + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + } + } + + SplitButton { + id: splitButton + + type: SplitButton.Tonal + stateLayer.onClicked: splitButton.expanded = !splitButton.expanded + menu.onItemSelected: item => root.selected(item) + } + } +} diff --git a/modules/nexus/common/SliderRow.qml b/modules/nexus/common/SliderRow.qml new file mode 100644 index 00000000..21cbe545 --- /dev/null +++ b/modules/nexus/common/SliderRow.qml @@ -0,0 +1,88 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus.common + +ConnectedRect { + id: root + + property alias icon: icon.text + property alias label: label.text + property alias valueLabel: valueLabel.text + property real value + + signal moved(value: real) + + implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins + rowLayout.anchors.topMargin + + RowLayout { + id: rowLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.largeIncreased + anchors.topMargin: Tokens.padding.large + spacing: Tokens.spacing.medium + + MaterialIcon { + id: icon + + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.icon.medium + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.medium + + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.small + + StyledText { + id: label + + Layout.fillWidth: true + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + id: valueLabel + + color: Colours.palette.m3outline + font: Tokens.font.body.small + } + } + + CustomMouseArea { + function onWheel(event: WheelEvent): void { + const step = GlobalConfig.services.audioIncrement; + if (event.angleDelta.y > 0) + root.moved(Math.min(1, root.value + step)); + else if (event.angleDelta.y < 0) + root.moved(Math.max(0, root.value - step)); + } + + Layout.fillWidth: true + implicitHeight: Tokens.padding.medium * 2 + + StyledSlider { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + implicitHeight: parent.implicitHeight + + radius: Tokens.rounding.small + value: root.value + enabled: root.enabled + onInteraction: v => root.moved(v) + } + } + } + } +} diff --git a/modules/nexus/common/StackPage.qml b/modules/nexus/common/StackPage.qml new file mode 100644 index 00000000..f104652e --- /dev/null +++ b/modules/nexus/common/StackPage.qml @@ -0,0 +1,122 @@ +import QtQuick +import QtQuick.Controls +import Caelestia.Config +import qs.components +import qs.modules.nexus + +StackView { + id: root + + required property NexusState nState + default property list pages + readonly property int animMovement: Tokens.padding.extraExtraLarge * 2 + + function openSubPage(idx: int, immediate: bool): void { + const page = pages[idx]; + if (page) { + push(page, { + nState + }, immediate ? StackView.Immediate : StackView.PushTransition); + } else { + console.warn(logCat, "Attempted to open invalid sub-page index", idx); + nState.closeSubPage(); + } + } + + clip: busy + + Component.onCompleted: { + openSubPage(0, true); + for (const page of nState.subPageIdxStack) + openSubPage(page, true); + } + + pushEnter: Transition { + SequentialAnimation { + PropertyAction { + property: "opacity" + value: 0 + } + PauseAnimation { + duration: Tokens.anim.durations.expressiveDefaultEffects + } + ParallelAnimation { + Anim { + property: "opacity" + to: 1 + type: Anim.SlowEffects + } + Anim { + property: "x" + from: root.animMovement + to: 0 + type: Anim.SlowEffects + } + } + } + } + + pushExit: Transition { + Anim { + property: "opacity" + to: 0 + type: Anim.DefaultEffects + } + } + + popEnter: Transition { + SequentialAnimation { + PropertyAction { + property: "opacity" + value: 0 + } + PauseAnimation { + duration: Tokens.anim.durations.expressiveDefaultEffects + } + ParallelAnimation { + Anim { + property: "opacity" + to: 1 + type: Anim.SlowEffects + } + Anim { + property: "x" + from: -root.animMovement + to: 0 + type: Anim.SlowEffects + } + } + } + } + + popExit: Transition { + Anim { + property: "opacity" + to: 0 + type: Anim.DefaultEffects + } + } + + LoggingCategory { + id: logCat + + name: "caelestia.nexus" + defaultLogLevel: LoggingCategory.Info + } + + Connections { + function onSubPageOpened(idx: int): void { + root.openSubPage(idx, false); + } + + function onSubPageClosed(): void { + if (root.depth < root.nState.subPageIdxStack.length) { + console.log(logCat, "Attempted to close page while depth < stack depth. Ignoring."); + return; + } + root.pop(); + } + + target: root.nState + } +} diff --git a/modules/nexus/common/StepperRow.qml b/modules/nexus/common/StepperRow.qml new file mode 100644 index 00000000..f15afa83 --- /dev/null +++ b/modules/nexus/common/StepperRow.qml @@ -0,0 +1,64 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus.common + +ConnectedRect { + id: root + + property alias label: label.text + property string subtext + property real value + property real from: 0 + property real to: 99 + property real stepSize: 1 + + signal moved(value: real) + + implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins * 2 + + RowLayout { + id: rowLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + id: label + + Layout.fillWidth: true + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + visible: root.subtext + text: root.subtext + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + } + } + + CustomSpinBox { + min: root.from + max: root.to + step: root.stepSize + value: root.value + onValueModified: v => root.moved(v) + } + } +} diff --git a/modules/nexus/common/ToggleRow.qml b/modules/nexus/common/ToggleRow.qml new file mode 100644 index 00000000..1afdcdac --- /dev/null +++ b/modules/nexus/common/ToggleRow.qml @@ -0,0 +1,79 @@ +import QtQuick +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +StyledSwitch { + id: root + + property string subtext + property alias first: bg.first + property alias last: bg.last + readonly property alias bg: bg + + horizontalPadding: Tokens.padding.largeIncreased + verticalPadding: Tokens.padding.medium + font: Tokens.font.body.small + + implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2 + implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2 + cLayer: 2 + + indicator.anchors.verticalCenter: verticalCenter + indicator.anchors.right: right + indicator.anchors.rightMargin: root.horizontalPadding + + onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY) + + background: ConnectedRect { + id: bg + + StateLayer { + id: stateLayer + + manualPressOverride: root.pressed + } + } + + contentItem: Item { + anchors.left: parent.left + anchors.right: root.indicator.left + anchors.leftMargin: root.horizontalPadding + anchors.rightMargin: Tokens.spacing.medium + + implicitWidth: column.implicitWidth + implicitHeight: column.implicitHeight + + Column { + id: column + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + StyledText { + id: label + + anchors.left: parent.left + anchors.right: parent.right + + text: root.text + font: root.font + elide: Text.ElideRight + } + + StyledText { + anchors.left: parent.left + anchors.right: parent.right + + visible: root.subtext + text: root.subtext + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + } + } + } +} diff --git a/modules/nexus/common/WallItem.qml b/modules/nexus/common/WallItem.qml new file mode 100644 index 00000000..39dd22e3 --- /dev/null +++ b/modules/nexus/common/WallItem.qml @@ -0,0 +1,105 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +Item { + id: root + + property alias source: img.source + property alias text: label.text + property alias radius: imgWrapper.radius + property alias imgHeight: imgWrapper.implicitHeight + property bool fillLabel: true + + signal clicked + + Layout.fillWidth: true + implicitHeight: layout.implicitHeight + + ColumnLayout { + id: layout + + anchors.fill: parent + spacing: Tokens.spacing.small + + StyledClippingRect { + id: imgWrapper + + Layout.fillWidth: true + implicitHeight: width + radius: Tokens.rounding.largeIncreased + color: Colours.tPalette.m3surfaceContainer + + Loader { + anchors.centerIn: parent + + opacity: img.status === Image.Ready ? 0 : 1 + active: opacity > 0 + + sourceComponent: StyledRect { + implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2 + implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2 + + color: Colours.palette.m3primaryContainer + radius: Tokens.rounding.full + + LoadingIndicator { + id: loadingIndicator + + anchors.centerIn: parent + containsIcon: true + implicitSize: Math.min(imgWrapper.width, imgWrapper.height) * 0.3 + } + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Image { + id: img + + anchors.fill: parent + asynchronous: true + fillMode: Image.PreserveAspectCrop + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } + retainWhileLoading: true + opacity: status === Image.Ready ? 1 : 0 + + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } + } + } + + StyledText { + id: label + + Layout.bottomMargin: Tokens.padding.small + Layout.fillWidth: true + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.label.builders.small.weight(Font.Medium).build() + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + } + + StateLayer { + anchors.bottomMargin: root.fillLabel ? 0 : layout.implicitHeight - imgWrapper.implicitHeight + onClicked: root.clicked() + } +} diff --git a/modules/nexus/navpane/NavLocations.qml b/modules/nexus/navpane/NavLocations.qml new file mode 100644 index 00000000..d559c443 --- /dev/null +++ b/modules/nexus/navpane/NavLocations.qml @@ -0,0 +1,130 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.containers +import qs.services +import qs.modules.nexus + +VerticalFadeFlickable { + id: root + + required property NexusState nState + + topMargin: Tokens.padding.large + bottomMargin: Tokens.padding.large + contentHeight: content.implicitHeight + + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + spacing: Tokens.spacing.extraSmall + + Repeater { + id: list + + model: PageRegistry.pages + + StyledRect { + id: item + + required property var modelData + required property int index + + readonly property bool isCurrentPage: index === root.nState.currentPageIdx + readonly property bool isCategoryStart: index === 0 || PageRegistry.pages[index - 1].category !== modelData.category + readonly property bool isCategoryEnd: index === list.model.length - 1 || PageRegistry.pages[index + 1].category !== modelData.category + + Layout.fillWidth: true + Layout.topMargin: index !== 0 && isCategoryStart ? Tokens.spacing.medium : 0 + implicitHeight: { + const h = layout.implicitHeight + layout.anchors.margins * 2; + return h % 2 === 0 ? h : h + 1; + } + + color: isCurrentPage ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + + topLeftRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLargeIncreased : isCategoryStart ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + topRightRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLargeIncreased : isCategoryStart ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + bottomLeftRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLargeIncreased : isCategoryEnd ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + bottomRightRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLargeIncreased : isCategoryEnd ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + + RadiusBehavior on topLeftRadius {} + RadiusBehavior on topRightRadius {} + RadiusBehavior on bottomLeftRadius {} + RadiusBehavior on bottomRightRadius {} + + StateLayer { + id: stateLayer + + anchors.fill: parent + topLeftRadius: parent.topLeftRadius + topRightRadius: parent.topRightRadius + bottomLeftRadius: parent.bottomLeftRadius + bottomRightRadius: parent.bottomRightRadius + + onClicked: root.nState.currentPageIdx = item.index + } + + RowLayout { + id: layout + + anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.medium + + StyledRect { + Layout.fillHeight: true + Layout.topMargin: -1 + Layout.bottomMargin: -1 + implicitWidth: height + + radius: Tokens.rounding.full + color: item.isCurrentPage ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer + + MaterialIcon { + anchors.centerIn: parent + anchors.verticalCenterOffset: 1 + + text: item.modelData.icon + color: item.isCurrentPage ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer + fontStyle: Tokens.font.icon.builders.medium.weight(Font.Medium).build() + grade: 25 + fill: item.modelData.noFill ? 0 : 1 + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + Layout.fillWidth: true + text: item.modelData.label + font: Tokens.font.body.medium + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + text: item.modelData.description + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.label.small + elide: Text.ElideRight + } + } + } + } + } + } + + component RadiusBehavior: Behavior { + Anim { + type: Anim.DefaultEffects + } + } +} diff --git a/modules/nexus/navpane/SearchBar.qml b/modules/nexus/navpane/SearchBar.qml new file mode 100644 index 00000000..6f35061d --- /dev/null +++ b/modules/nexus/navpane/SearchBar.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus + +StyledRect { + id: root + + required property NexusState nState + + implicitHeight: searchLayout.implicitHeight + Tokens.padding.medium * 2 + + radius: Tokens.rounding.full + color: Colours.tPalette.m3surfaceContainerLowest + border.color: Colours.palette.m3outlineVariant + + Behavior on border.color { + CAnim {} + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.IBeamCursor + onClicked: searchField.focus = true + } + + RowLayout { + id: searchLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.large + + spacing: Tokens.spacing.small + + MaterialIcon { + text: "search" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.medium + } + + StyledTextField { + id: searchField + + Layout.fillWidth: true + Layout.fillHeight: true + + placeholderText: qsTr("Search settings") + placeholderTextColor: Colours.palette.m3onSurfaceVariant + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.large + + Binding { + target: root.nState + property: "searchOpen" + value: searchField.text.length > 0 + } + } + + IconButton { + icon: "close" + font: Tokens.font.icon.medium + type: IconButton.Text + padding: Tokens.padding.extraSmall + isRound: true + onClicked: searchField.clear() + + opacity: searchField.text.length > 0 ? 1 : 0 + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + } +} diff --git a/modules/nexus/pages/AboutPage.qml b/modules/nexus/pages/AboutPage.qml new file mode 100644 index 00000000..c3c7b5d5 --- /dev/null +++ b/modules/nexus/pages/AboutPage.qml @@ -0,0 +1,157 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Io +import Caelestia +import Caelestia.Config +import qs.components +import qs.services +import qs.utils +import qs.modules.nexus.common + +PageBase { + id: root + + // Plugin support is not wired up yet; always 0 for now + readonly property int pluginCount: 0 + + property string quickshellVersion + property string cliVersion + + title: qsTr("About") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // e.g. "Quickshell 0.3.0 (revision ...)" + Process { + running: true + command: ["quickshell", "--version"] + stdout: StdioCollector { + onStreamFinished: root.quickshellVersion = text.trim().split(" ")[1] ?? "" + } + } + + // Parsed from the caelestia CLI's package listing; the sh wrapper avoids a + // warning when the (optional) CLI isn't installed + Process { + running: true + command: ["sh", "-c", "caelestia --version 2>/dev/null"] + stdout: StdioCollector { + onStreamFinished: { + const m = text.match(/caelestia-cli\S*\s+(\d+(?:\.\d+)*)/); + root.cliVersion = m ? m[1] : ""; + } + } + } + + // Hero + ConnectedRect { + Layout.fillWidth: true + first: true + last: true + implicitHeight: hero.implicitHeight + Tokens.padding.extraLarge * 2 + + ColumnLayout { + id: hero + + anchors.centerIn: parent + width: parent.width - Tokens.padding.largeIncreased * 2 + spacing: Tokens.spacing.small + + AnimatedLogo { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: implicitWidth + Layout.preferredHeight: implicitHeight + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Tokens.spacing.small + text: "Caelestia" + font: Tokens.font.headline.builders.large.width(110).build() + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: CUtils.version ? `v${CUtils.version}` : "…" + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.medium + } + } + } + + // System + SectionHeader { + text: qsTr("System") + } + + InfoRow { + first: true + label: qsTr("Hostname") + value: SysInfo.hostname + } + + InfoRow { + label: qsTr("Device") + value: SysInfo.device + } + + InfoRow { + label: qsTr("Distro") + value: SysInfo.osPrettyName || SysInfo.osName + } + + InfoRow { + label: qsTr("Kernel") + value: SysInfo.kernel + } + + InfoRow { + last: true + label: qsTr("Firmware") + value: SysInfo.firmware + } + + // Software + SectionHeader { + text: qsTr("Software") + } + + InfoRow { + first: true + label: qsTr("Shell") + value: CUtils.version || "…" + } + + InfoRow { + label: qsTr("CLI") + value: root.cliVersion || "…" + } + + InfoRow { + label: qsTr("Quickshell") + value: root.quickshellVersion || "…" + } + + InfoRow { + last: true + label: qsTr("Qt") + value: CUtils.qtVersion || "…" + } + + // Plugins + SectionHeader { + text: qsTr("Plugins") + } + + InfoRow { + first: true + last: true + label: qsTr("Loaded plugins") + value: root.pluginCount.toString() + } + } +} diff --git a/modules/nexus/pages/AudioPage.qml b/modules/nexus/pages/AudioPage.qml new file mode 100644 index 00000000..8ae3f9cc --- /dev/null +++ b/modules/nexus/pages/AudioPage.qml @@ -0,0 +1,134 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services +import qs.utils +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Audio") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // Output + SliderRow { + Layout.fillWidth: true + first: true + icon: Icons.getVolumeIcon(Audio.volume, Audio.muted) + label: qsTr("Output") + valueLabel: Math.round(value * 100) + "%" + value: Audio.volume + enabled: !Audio.muted + onMoved: v => Audio.setVolume(v) + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Muted") + checked: Audio.muted + onToggled: Audio.setStreamMuted(Audio.sink, checked) + } + + AudioDeviceList { + nodes: Audio.sinks + currentId: Audio.sink?.id ?? -1 + iconName: "speaker" + placeholderIcon: "speaker" + placeholderText: qsTr("No output devices") + onSelected: node => Audio.setAudioSink(node) + } + + // Input + SliderRow { + Layout.fillWidth: true + Layout.topMargin: Tokens.spacing.large - parent.spacing + first: true + icon: Icons.getMicVolumeIcon(Audio.sourceVolume, Audio.sourceMuted) + label: qsTr("Input") + valueLabel: Math.round(value * 100) + "%" + value: Audio.sourceVolume + enabled: !Audio.sourceMuted + onMoved: v => Audio.setSourceVolume(v) + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Muted") + checked: Audio.sourceMuted + onToggled: Audio.setStreamMuted(Audio.source, checked) + } + + AudioDeviceList { + nodes: Audio.sources + currentId: Audio.source?.id ?? -1 + iconName: "mic" + placeholderIcon: "mic_off" + placeholderText: qsTr("No input devices") + onSelected: node => Audio.setAudioSource(node) + } + + // Per-app volumes + ConnectedRect { + Layout.fillWidth: true + Layout.topMargin: Tokens.spacing.large - parent.spacing + implicitHeight: appLayout.implicitHeight + appLayout.anchors.margins * 2 + first: true + last: true + + StateLayer { + onClicked: root.nState.openSubPage(1) + } + + RowLayout { + id: appLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + MaterialIcon { + text: "tune" + font: Tokens.font.icon.medium + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + Layout.fillWidth: true + text: qsTr("App volumes") + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + text: Audio.streams.length === 0 ? qsTr("No apps playing audio") : Audio.streams.length === 1 ? qsTr("1 app playing audio") : qsTr("%1 apps playing audio").arg(Audio.streams.length) + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + animate: true + } + } + + MaterialIcon { + text: "chevron_right" + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.icon.medium + } + } + } + } +} diff --git a/modules/nexus/pages/BluetoothPage.qml b/modules/nexus/pages/BluetoothPage.qml new file mode 100644 index 00000000..6fac102d --- /dev/null +++ b/modules/nexus/pages/BluetoothPage.qml @@ -0,0 +1,248 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.utils +import qs.modules.nexus.common + +PageBase { + id: root + + readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter // qmllint disable unresolved-type + readonly property bool btEnabled: adapter?.enabled ?? false + + title: qsTr("Connected devices") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Bluetooth") + font: Tokens.font.body.medium + horizontalPadding: Tokens.padding.largeIncreased + checked: root.btEnabled + onToggled: { + if (root.adapter) + root.adapter.enabled = checked; + } + } + + ItemList { + id: savedList + + showList: root.btEnabled + placeholderIcon: root.btEnabled ? "devices_other" : "bluetooth_disabled" + placeholderText: root.btEnabled ? qsTr("No saved devices") : qsTr("Bluetooth disabled") + + model: ScriptModel { + values: Bluetooth.devices.values.filter(d => d.bonded).sort((a, b) => (b.connected - a.connected) || a.name.localeCompare(b.name)) // qmllint disable unresolved-type + } + + delegate: StyledRect { + id: device + + required property BluetoothDevice modelData + readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type + readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting) // qmllint disable unresolved-type + property real textOpacity: loading ? 0.5 : 1 + + anchors.left: savedList.list.contentItem.left + anchors.right: savedList.list.contentItem.right + implicitHeight: deviceLayout.implicitHeight + deviceLayout.anchors.margins * 2 + radius: Tokens.rounding.extraSmall + color: "transparent" + + Behavior on textOpacity { + Anim { + type: Anim.DefaultEffects + } + } + + StateLayer { + disabled: device.loading + onClicked: { + if (!device.modelData || device.loading) + return; + device.modelData.connected = !device.connected; + } + } + + RowLayout { + id: deviceLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: deviceIcon.implicitHeight + Tokens.padding.small * 2 + radius: Tokens.rounding.full + color: device.connected ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer + + MaterialIcon { + id: deviceIcon + + anchors.centerIn: parent + text: Icons.getBluetoothIcon(device.modelData?.icon ?? "") + color: device.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer + font: Tokens.font.icon.medium + fill: device.connected ? 1 : 0 + opacity: device.textOpacity + + Behavior on fill { + Anim {} + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + opacity: device.textOpacity + + StyledText { + Layout.fillWidth: true + text: device.modelData?.name ?? qsTr("Unknown") + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + text: device.connected ? qsTr("Connected%1").arg(device.modelData?.batteryAvailable ? " • " + Math.round(device.modelData.battery * 100) + "%" : "") : qsTr("Saved") + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + animate: true + } + } + + Item { + Layout.fillHeight: true + implicitWidth: height + + AnimLoader { + anchors.centerIn: parent + sourceComp: device.loading ? loadingComp : btnComp + + Component { + id: btnComp + + IconButton { + icon: "settings" + type: IconButton.Text + padding: Tokens.padding.small + inactiveOnColour: device.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + label.fill: 0 + + onClicked: { + root.nState.selectedBtDevice = device.modelData; + root.nState.openSubPage(1); // Per device info page + } + } + } + + Component { + id: loadingComp + + LoadingIndicator { + implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3) + } + } + } + } + } + } + } + + ConnectedRect { + Layout.fillWidth: true + implicitHeight: pairLayout.implicitHeight + pairLayout.anchors.margins * 2 + last: true + + StateLayer { + disabled: !root.btEnabled + onClicked: root.nState.openSubPage(2) + } + + RowLayout { + id: pairLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + opacity: root.btEnabled ? 1 : 0.5 + + Behavior on opacity { + Anim {} + } + + MaterialIcon { + text: "add" + font: Tokens.font.icon.medium + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Pair new device") + font: Tokens.font.body.small + elide: Text.ElideRight + } + } + } + + ToggleRow { + Layout.fillWidth: true + Layout.topMargin: Tokens.spacing.large - parent.spacing + + first: true + text: qsTr("Discoverable") + subtext: qsTr("Allow nearby devices to find this one") + enabled: root.btEnabled + opacity: root.btEnabled ? 1 : 0.5 + checked: root.adapter?.discoverable ?? false + onToggled: { + if (root.adapter) + root.adapter.discoverable = checked; + } + + Behavior on opacity { + Anim {} + } + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Pairable") + subtext: qsTr("Allow nearby devices to pair with this one") + enabled: root.btEnabled + opacity: root.btEnabled ? 1 : 0.5 + checked: root.adapter?.pairable ?? false + onToggled: { + if (root.adapter) + root.adapter.pairable = checked; + } + + Behavior on opacity { + Anim {} + } + } + } +} diff --git a/modules/nexus/pages/LanguageAndRegion.qml b/modules/nexus/pages/LanguageAndRegion.qml new file mode 100644 index 00000000..b31fc825 --- /dev/null +++ b/modules/nexus/pages/LanguageAndRegion.qml @@ -0,0 +1,175 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus.common + +PageBase { + id: root + + // Temperature units (index 0 = Celsius, 1 = Fahrenheit — matches Weather.formatTemp) + readonly property list tempItems: [ + MenuItem { + text: "°C" + }, + MenuItem { + text: "°F" + } + ] + + // Clock format (index 0 = 24-hour, 1 = 12-hour — matches Time.useTwelveHourClock) + readonly property list clockItems: [ + MenuItem { + text: qsTr("24-hour") + }, + MenuItem { + text: qsTr("12-hour") + } + ] + + title: qsTr("Language & region") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // Language + SectionHeader { + first: true + text: qsTr("Language") + } + + // Read-only: the shell follows the system locale (no in-shell translations yet) + ConnectedRect { + Layout.fillWidth: true + first: true + last: true + implicitHeight: localeLayout.implicitHeight + localeLayout.anchors.margins * 2 + + RowLayout { + id: localeLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + Layout.fillWidth: true + text: qsTr("System language") + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Follows your system locale (%1)").arg(Qt.locale().name) + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + } + } + + StyledText { + text: Qt.locale().nativeLanguageName || Qt.locale().name + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.small + } + } + } + + // Weather + SectionHeader { + text: qsTr("Weather") + } + + // Placeholder until the map-based location picker lands + ConnectedRect { + Layout.fillWidth: true + first: true + last: true + implicitHeight: comingSoon.implicitHeight + Tokens.padding.extraLarge * 2 + + ColumnLayout { + id: comingSoon + + anchors.centerIn: parent + width: parent.width - Tokens.padding.largeIncreased * 2 + spacing: Tokens.padding.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "map" + color: Colours.palette.m3outlineVariant + font: Tokens.font.icon.extraLarge + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Location picker coming soon") + color: Colours.palette.m3outlineVariant + font: Tokens.font.title.small + } + + StyledText { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + text: qsTr("Choose your weather location on a map in a future update") + color: Colours.palette.m3outlineVariant + font: Tokens.font.body.small + } + } + } + + // Units + SectionHeader { + text: qsTr("Units") + } + + SelectRow { + Layout.fillWidth: true + first: true + label: qsTr("Temperature") + subtext: qsTr("Units for weather temperatures") + menuItems: root.tempItems + active: root.tempItems[GlobalConfig.services.useFahrenheit ? 1 : 0] + onSelected: item => GlobalConfig.services.useFahrenheit = root.tempItems.indexOf(item) === 1 + } + + SelectRow { + Layout.fillWidth: true + last: true + label: qsTr("System temperatures") + subtext: qsTr("Units for CPU and GPU temperatures") + menuItems: root.tempItems + active: root.tempItems[GlobalConfig.services.useFahrenheitPerformance ? 1 : 0] + onSelected: item => GlobalConfig.services.useFahrenheitPerformance = root.tempItems.indexOf(item) === 1 + } + + // Time & date + SectionHeader { + text: qsTr("Time & date") + } + + SelectRow { + Layout.fillWidth: true + first: true + last: true + label: qsTr("Clock format") + subtext: qsTr("How times are shown across the shell") + menuItems: root.clockItems + active: root.clockItems[GlobalConfig.services.useTwelveHourClock ? 1 : 0] + onSelected: item => GlobalConfig.services.useTwelveHourClock = root.clockItems.indexOf(item) === 1 + } + } +} diff --git a/modules/nexus/pages/NetworkPage.qml b/modules/nexus/pages/NetworkPage.qml new file mode 100644 index 00000000..b595a64d --- /dev/null +++ b/modules/nexus/pages/NetworkPage.qml @@ -0,0 +1,235 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.utils +import qs.modules.nexus.common + +PageBase { + id: root + + signal networkSelected(ap: Nmcli.AccessPoint) + + title: qsTr("Network") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + Timer { + running: root.visible && Nmcli.wifiEnabled + repeat: true + triggeredOnStart: true + interval: GlobalConfig.nexus.networkRescanInterval + onTriggered: Nmcli.rescanWifi() + } + + Timer { + id: wifiScanDelay + + interval: 100 + onTriggered: Nmcli.rescanWifi() + } + + Connections { + function onWifiEnabledChanged(): void { + if (Nmcli.wifiEnabled) + wifiScanDelay.start(); + } + + target: Nmcli + } + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Wi-Fi") + font: Tokens.font.body.medium + horizontalPadding: Tokens.padding.largeIncreased + checked: Nmcli.wifiEnabled + onToggled: Nmcli.enableWifi(checked) + } + + ItemList { + id: networkList + + showList: Nmcli.wifiEnabled + placeholderIcon: Nmcli.wifiEnabled ? "wifi_find" : "signal_wifi_off" + placeholderText: Nmcli.wifiEnabled ? qsTr("No networks found") : qsTr("Wi-Fi disabled") + extraHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0 // Inline so it isn't affected by anim + list.anchors.top: scanningIndicator.bottom + + model: ScriptModel { + values: { + const connecting = Nmcli.connectingSsid(); + // Lower rank sorts higher in the list + const rank = n => n.active ? 0 : n.ssid === connecting ? 1 : Nmcli.hasSavedProfile(n.ssid) ? 2 : 3; + return [...Nmcli.networks].sort((a, b) => rank(a) - rank(b) || b.strength - a.strength); + } + } + + delegate: StateLayer { + id: network + + required property Nmcli.AccessPoint modelData + property bool currentSelected + property real textOpacity: disabled ? 0.5 : 1 + + disabled: currentSelected || Nmcli.connectingSsid() === modelData.ssid + + anchors.left: networkList.list.contentItem.left + anchors.right: networkList.list.contentItem.right + implicitHeight: networkLayout.implicitHeight + networkLayout.anchors.margins * 2 + radius: Tokens.rounding.extraSmall + anchors.fill: undefined + + onClicked: { + if (!modelData.active) { + NetworkConnection.handleConnect(modelData); + currentSelected = true; + root.networkSelected(modelData); + } + } + + Behavior on textOpacity { + Anim { + type: Anim.DefaultEffects + } + } + + Connections { + function onActiveChanged(): void { + if (network.modelData.active) + network.currentSelected = false; + } + + target: network.modelData + } + + Connections { + function onNetworkSelected(ap: Nmcli.AccessPoint): void { + if (ap !== network.modelData) + network.currentSelected = false; + } + + target: root + } + + RowLayout { + id: networkLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.large + anchors.leftMargin: Tokens.padding.extraLarge + anchors.rightMargin: Tokens.padding.extraLarge + spacing: Tokens.spacing.medium + + MaterialIcon { + text: Icons.getNetworkIcon(network.modelData.strength) + color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + font: Tokens.font.icon.medium + opacity: network.textOpacity + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + opacity: network.textOpacity + + StyledText { + Layout.fillWidth: true + text: network.modelData.ssid + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Security: %1%2").arg(network.modelData.security).arg(Nmcli.hasSavedProfile(network.modelData.ssid) ? qsTr(" • Saved") : "") + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + } + } + + AnimLoader { + sourceComp: Nmcli.connectingSsid() === network.modelData.ssid ? loadingComp : iconComp + + Component { + id: iconComp + + MaterialIcon { + text: network.modelData.active ? "settings" : "lock" + color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + font: Tokens.font.icon.medium + opacity: network.textOpacity + } + } + + Component { + id: loadingComp + + LoadingIndicator { + implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3) + } + } + } + } + } + + StyledProgressBar { + id: scanningIndicator + + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 1 + implicitHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0 + indeterminate: true + + Behavior on implicitHeight { + Anim { + type: Anim.DefaultEffects + } + } + } + } + + ConnectedRect { + Layout.fillWidth: true + implicitHeight: addNetworkLayout.implicitHeight + addNetworkLayout.anchors.margins * 2 + last: true + + StateLayer {} + + RowLayout { + id: addNetworkLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + + spacing: Tokens.spacing.medium + + MaterialIcon { + text: "add" + font: Tokens.font.icon.medium + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Add network") + font: Tokens.font.body.small + elide: Text.ElideRight + } + } + } + } +} diff --git a/modules/nexus/pages/PanelsPage.qml b/modules/nexus/pages/PanelsPage.qml new file mode 100644 index 00000000..d49879be --- /dev/null +++ b/modules/nexus/pages/PanelsPage.qml @@ -0,0 +1,46 @@ +import QtQuick.Layouts +import Caelestia.Config +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Panels") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + NavRow { + first: true + icon: "dashboard" + label: qsTr("Dashboard") + status: Config.dashboard.enabled ? qsTr("Enabled") : qsTr("Disabled") + onClicked: root.nState.openSubPage(1) + } + + NavRow { + icon: "dock_to_bottom" + label: qsTr("Taskbar") + status: Config.bar.persistent ? qsTr("Always visible") : Config.bar.showOnHover ? qsTr("Reveal on hover") : qsTr("Reveal on drag") + onClicked: root.nState.openSubPage(2) + } + + NavRow { + icon: "apps" + label: qsTr("Launcher") + status: Config.launcher.enabled ? qsTr("Enabled") : qsTr("Disabled") + onClicked: root.nState.openSubPage(3) + } + + NavRow { + last: true + icon: "dock_to_right" + label: qsTr("Sidebar") + status: Config.sidebar.enabled ? qsTr("Enabled") : qsTr("Disabled") + onClicked: root.nState.openSubPage(4) + } + } +} diff --git a/modules/nexus/pages/ServicesPage.qml b/modules/nexus/pages/ServicesPage.qml new file mode 100644 index 00000000..16c92d35 --- /dev/null +++ b/modules/nexus/pages/ServicesPage.qml @@ -0,0 +1,225 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus.common + +PageBase { + id: root + + // Lyrics backends, ordered to match LyricsBackend::Backend (Auto, Local, LRCLIB, NetEase) + readonly property list lyricsItems: [ + MenuItem { + text: qsTr("Auto") + }, + MenuItem { + text: "Local" + }, + MenuItem { + text: "LRCLIB" + }, + MenuItem { + text: "NetEase" + } + ] + + // GPU options + the config string each maps to (see Gpu::parseType) + readonly property list gpuItems: [ + MenuItem { + text: qsTr("Auto") + }, + MenuItem { + text: "NVIDIA" + }, + MenuItem { + text: qsTr("Generic") + }, + MenuItem { + text: qsTr("None") + } + ] + readonly property list gpuValues: ["", "NVIDIA", "GENERIC", "None"] + + function gpuKeyToIndex(key: string): int { + const u = (key ?? "").trim().toUpperCase(); + if (u === "") + return 0; // Auto + if (u === "NVIDIA") + return 1; + if (u === "GENERIC") + return 2; + return 3; // None + } + + title: qsTr("Services") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // Detected running players, used as default-player options + Variants { + id: playerVariants + + model: [...new Set(Players.list.map(p => Players.getIdentity(p)).filter(id => id))] + + MenuItem { + required property string modelData + + text: modelData + icon: modelData === GlobalConfig.services.defaultPlayer ? "check" : "" + activeIcon: "music_note" + } + } + + // Polling + SectionHeader { + first: true + text: qsTr("Polling") + } + + StepperRow { + Layout.fillWidth: true + first: true + label: qsTr("Media refresh") + subtext: qsTr("How often the media position updates (ms)") + value: GlobalConfig.dashboard.mediaUpdateInterval + from: 100 + to: 2000 + stepSize: 50 + onMoved: v => GlobalConfig.dashboard.mediaUpdateInterval = v + } + + StepperRow { + Layout.fillWidth: true + label: qsTr("System stats refresh") + subtext: qsTr("CPU, memory and GPU update interval (seconds)") + value: GlobalConfig.dashboard.resourceUpdateInterval / 1000 + from: 0.5 + to: 10 + stepSize: 0.5 + onMoved: v => GlobalConfig.dashboard.resourceUpdateInterval = Math.round(v * 1000) + } + + StepperRow { + Layout.fillWidth: true + last: true + label: qsTr("Wi-Fi rescan") + subtext: qsTr("How often available networks are rescanned (seconds)") + value: GlobalConfig.nexus.networkRescanInterval / 1000 + from: 5 + to: 120 + stepSize: 5 + onMoved: v => GlobalConfig.nexus.networkRescanInterval = Math.round(v * 1000) + } + + // Media & lyrics + SectionHeader { + text: qsTr("Media & lyrics") + } + + SelectRow { + Layout.fillWidth: true + first: true + label: qsTr("Lyrics backend") + subtext: qsTr("Source used to fetch synced lyrics") + menuItems: root.lyricsItems + active: root.lyricsItems[Lyrics.preferredBackend] ?? root.lyricsItems[0] + onSelected: item => Lyrics.preferredBackend = root.lyricsItems.indexOf(item) + } + + SelectRow { + Layout.fillWidth: true + last: true + label: qsTr("Default player") + subtext: qsTr("Preferred media player when several are open") + menuItems: playerVariants.instances + active: menuItems.find(i => i.text === GlobalConfig.services.defaultPlayer) ?? null + fallbackIcon: "music_note" + fallbackText: GlobalConfig.services.defaultPlayer || qsTr("Auto") + onSelected: item => GlobalConfig.services.defaultPlayer = item.text + } + + // Input increments + SectionHeader { + text: qsTr("Input increments") + } + + StepperRow { + Layout.fillWidth: true + first: true + label: qsTr("Volume step") + subtext: qsTr("Amount the volume changes per scroll (%)") + value: Math.round(GlobalConfig.services.audioIncrement * 100) + from: 1 + to: 50 + stepSize: 1 + onMoved: v => GlobalConfig.services.audioIncrement = v / 100 + } + + StepperRow { + Layout.fillWidth: true + label: qsTr("Brightness step") + subtext: qsTr("Amount the brightness changes per scroll (%)") + value: Math.round(GlobalConfig.services.brightnessIncrement * 100) + from: 1 + to: 50 + stepSize: 1 + onMoved: v => GlobalConfig.services.brightnessIncrement = v / 100 + } + + StepperRow { + Layout.fillWidth: true + last: true + label: qsTr("Max volume") + subtext: qsTr("Upper limit for output volume (%)") + value: Math.round(GlobalConfig.services.maxVolume * 100) + from: 50 + to: 200 + stepSize: 5 + onMoved: v => GlobalConfig.services.maxVolume = v / 100 + } + + // Service tuning + SectionHeader { + text: qsTr("Service tuning") + } + + StepperRow { + Layout.fillWidth: true + first: true + label: qsTr("Visualiser bars") + subtext: qsTr("Number of bars in the audio visualisers") + value: GlobalConfig.services.visualiserBars + from: 10 + to: 120 + stepSize: 2 + onMoved: v => GlobalConfig.services.visualiserBars = v + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Smart colour scheme") + subtext: qsTr("Derive theme mode and variant from the wallpaper") + checked: GlobalConfig.services.smartScheme + onToggled: GlobalConfig.services.smartScheme = checked + } + + SelectRow { + Layout.fillWidth: true + last: true + label: qsTr("GPU") + subtext: Gpu.name ? qsTr("Monitoring: %1").arg(Gpu.name) : qsTr("Override for GPU type") + menuOnTop: true + menuItems: root.gpuItems + active: root.gpuItems[root.gpuKeyToIndex(GlobalConfig.services.gpuType)] + onSelected: item => GlobalConfig.services.gpuType = root.gpuValues[root.gpuItems.indexOf(item)] + } + } +} diff --git a/modules/nexus/pages/WallpaperAndStyle.qml b/modules/nexus/pages/WallpaperAndStyle.qml new file mode 100644 index 00000000..98a5e413 --- /dev/null +++ b/modules/nexus/pages/WallpaperAndStyle.qml @@ -0,0 +1,202 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Components +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.components.images +import qs.services +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Wallpaper & style") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.large + + StyledClippingRect { + id: wallWrapper + + Layout.alignment: Qt.AlignHCenter + implicitWidth: { + const screen = root.nState.screen; + return implicitHeight / screen.height * screen.width; + } + implicitHeight: { + const screen = root.nState.screen; + const cWidth = root.cappedWidth; + return Math.min(Math.round(cWidth * 0.4), cWidth / screen.width * screen.height); + } + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.large + + Loader { + anchors.centerIn: parent + opacity: Config.background.wallpaperEnabled ? 0 : 1 + active: opacity > 0 + + sourceComponent: ColumnLayout { + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "hide_image" + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.icon.extraLarge + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Wallpaper disabled") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.large + } + } + + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } + } + + Item { + anchors.fill: parent + opacity: Config.background.wallpaperEnabled ? 1 : 0 + + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } + + Loader { + id: wallIndicatorLoader + + anchors.centerIn: parent + + opacity: 0 + active: opacity > 0 + + sourceComponent: StyledRect { + implicitWidth: wallLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2 + implicitHeight: wallLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2 + + color: Colours.palette.m3primaryContainer + radius: Tokens.rounding.full + + LoadingIndicator { + id: wallLoadingIndicator + + anchors.centerIn: parent + containsIcon: true + implicitSize: Math.min(wallWrapper.implicitWidth, wallWrapper.implicitHeight) * 0.4 + } + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Timer { + id: wallLoadDebounceTimer + + interval: 100 + onTriggered: { + if (wallImg.status !== Image.Ready) + wallIndicatorLoader.opacity = 1; + } + } + + FadeImage { + id: wallImg + + anchors.fill: parent + source: Wallpapers.current + preventInit: wallIndicatorLoader.opacity > 0 + fadeOutAnim: Anim.DefaultEffects + fadeInAnim: Anim.SlowEffects + + onSourceChanged: wallLoadDebounceTimer.restart() + + onStatusChanged: { + if (status === Image.Ready) { + wallLoadDebounceTimer.stop(); + wallIndicatorLoader.opacity = 0; + } + } + } + } + } + + ButtonRow { + Layout.alignment: Qt.AlignHCenter + spacing: Tokens.spacing.small + + IconTextButton { + icon: "wallpaper" + text: qsTr("Wallpapers") + font: Tokens.font.body.large + isRound: true + shapeMorph: true + type: IconTextButton.Tonal + horizontalPadding: Tokens.padding.extraLarge + verticalPadding: Tokens.padding.medium + disabled: !Config.background.wallpaperEnabled + onClicked: root.nState.openSubPage(1) // Wallpaper page + } + + IconTextButton { + icon: "palette" + text: qsTr("Colours") + font: Tokens.font.body.large + isRound: true + shapeMorph: true + type: IconTextButton.Tonal + horizontalPadding: Tokens.padding.extraLarge + verticalPadding: Tokens.padding.medium + onClicked: root.nState.openSubPage(3) // Colours page + } + } + + ToggleRow { + Layout.fillWidth: true + + first: true + text: qsTr("Display wallpaper") + checked: Config.background.wallpaperEnabled + onToggled: GlobalConfig.background.wallpaperEnabled = checked + } + + ToggleRow { + Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing + Layout.fillWidth: true + + text: qsTr("Transparency") + subtext: qsTr("Base %1, layers %2").arg(Colours.transparency.base).arg(Colours.transparency.layers) + checked: Colours.transparency.enabled + onToggled: GlobalConfig.appearance.transparency.enabled = checked + } + + ToggleRow { + Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing + Layout.fillWidth: true + + last: true + text: qsTr("Dark theme") + checked: !Colours.light + onToggled: Colours.setMode(checked ? "dark" : "light") + } + } +} diff --git a/modules/nexus/pages/audio/AppVolumes.qml b/modules/nexus/pages/audio/AppVolumes.qml new file mode 100644 index 00000000..8f36a4ca --- /dev/null +++ b/modules/nexus/pages/audio/AppVolumes.qml @@ -0,0 +1,70 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire +import Caelestia.Config +import qs.components +import qs.services +import qs.utils +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("App volumes") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + StyledText { + Layout.fillWidth: true + Layout.leftMargin: Tokens.padding.small + Layout.bottomMargin: Tokens.spacing.medium + text: qsTr("Adjust the volume of individual apps currently playing audio.") + color: Colours.palette.m3outline + font: Tokens.font.body.small + wrapMode: Text.WordWrap + } + + ItemList { + id: streamList + + first: true + last: true + showList: true + placeholderIcon: "music_off" + placeholderText: qsTr("No apps playing audio") + color: list.count === 0 ? Colours.tPalette.m3surfaceContainer : "transparent" + list.spacing: Tokens.spacing.extraSmall / 2 + + model: ScriptModel { + values: [...Audio.streams] + } + + delegate: SliderRow { + id: stream + + required property PwNode modelData + required property int index + + anchors.left: streamList.list.contentItem.left + anchors.right: streamList.list.contentItem.right + first: index === 0 + last: index === streamList.list.count - 1 + + icon: Icons.getVolumeIcon(stream.modelData?.audio?.volume ?? 0, stream.modelData?.audio?.muted ?? false) + label: Audio.getStreamName(stream.modelData) + valueLabel: Math.round(value * 100) + "%" + value: stream.modelData?.audio?.volume ?? 0 + enabled: !stream.modelData?.audio?.muted + onMoved: v => Audio.setStreamVolume(stream.modelData, v) + } + } + } +} diff --git a/modules/nexus/pages/bluetooth/BluetoothPairing.qml b/modules/nexus/pages/bluetooth/BluetoothPairing.qml new file mode 100644 index 00000000..548bff1a --- /dev/null +++ b/modules/nexus/pages/bluetooth/BluetoothPairing.qml @@ -0,0 +1,193 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.utils +import qs.modules.nexus.common + +PageBase { + id: root + + readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter // qmllint disable unresolved-type + + function setScan(on: bool): void { + if (adapter?.enabled) + adapter.discovering = on; + } + + title: qsTr("Pair new device") + isSubPage: true + + Component.onCompleted: setScan(true) + Component.onDestruction: setScan(false) + onVisibleChanged: setScan(visible) + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + Connections { + function onEnabledChanged(): void { + if (root.adapter && !root.adapter.enabled) + root.nState.closeSubPage(); + } + + target: root.adapter + } + + ConnectedRect { + Layout.fillWidth: true + implicitHeight: headerText.implicitHeight + Tokens.padding.medium * 2 + first: true + + StyledText { + id: headerText + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Tokens.padding.large + anchors.verticalCenterOffset: Math.round(fontInfo.pointSize * 0.2) + + text: qsTr("Available devices") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.small + } + } + + ItemList { + id: deviceList + + Layout.fillWidth: true + showList: true + extraHeight: scanIndicator.implicitHeight + last: true + placeholderIcon: "bluetooth_searching" + placeholderText: qsTr("Searching for devices…") + list.anchors.top: scanIndicator.bottom + + model: ScriptModel { + values: Bluetooth.devices.values.filter(d => !d.bonded).sort((a, b) => (b.pairing - a.pairing) || a.name.localeCompare(b.name)) // qmllint disable unresolved-type + } + + delegate: Item { + id: newDevice + + required property BluetoothDevice modelData + required property int index + property real textOpacity: modelData?.pairing ? 0.5 : 1 + property bool wasPairing + + anchors.left: deviceList.list.contentItem.left + anchors.right: deviceList.list.contentItem.right + implicitHeight: newLayout.implicitHeight + newLayout.anchors.margins * 2 + + Behavior on textOpacity { + Anim { + type: Anim.DefaultEffects + } + } + + Connections { + function onPairedChanged(): void { + if (newDevice.wasPairing && newDevice.modelData?.paired) + root.nState.closeSubPage(); + } + + target: newDevice.modelData + } + + StateLayer { + radius: Tokens.rounding.extraSmall + bottomLeftRadius: newDevice.index === deviceList?.list.count - 1 ? Tokens.rounding.extraLarge : radius + bottomRightRadius: newDevice.index === deviceList?.list.count - 1 ? Tokens.rounding.extraLarge : radius + disabled: newDevice.modelData?.pairing ?? false + + onClicked: { + newDevice.modelData?.pair(); + newDevice.wasPairing = true; + } + } + + RowLayout { + id: newLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + MaterialIcon { + text: Icons.getBluetoothIcon(newDevice.modelData?.icon ?? "") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.icon.medium + opacity: newDevice.textOpacity + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + opacity: newDevice.textOpacity + + StyledText { + Layout.fillWidth: true + text: newDevice.modelData?.name || qsTr("Unknown device") + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + text: newDevice.modelData?.pairing ? qsTr("Pairing...") : (newDevice.modelData?.address ?? "") + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + animate: true + } + } + + Loader { + asynchronous: true + active: opacity > 0 + opacity: newDevice.modelData?.pairing ? 1 : 0 + + sourceComponent: LoadingIndicator { + implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3) + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + } + } + + StyledProgressBar { + id: scanIndicator + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 1 + implicitHeight: Tokens.rounding.extraSmall + indeterminate: true + + Behavior on implicitHeight { + Anim { + type: Anim.DefaultEffects + } + } + } + } + } +} diff --git a/modules/nexus/pages/bluetooth/BtDeviceInfo.qml b/modules/nexus/pages/bluetooth/BtDeviceInfo.qml new file mode 100644 index 00000000..604973b6 --- /dev/null +++ b/modules/nexus/pages/bluetooth/BtDeviceInfo.qml @@ -0,0 +1,260 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Bluetooth +import Caelestia.Components +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus.common + +PageBase { + id: root + + readonly property BluetoothDevice device: nState.selectedBtDevice + readonly property bool connected: device?.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type + readonly property bool loading: device?.state === BluetoothDeviceState.Connecting || device?.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type + + readonly property string statusText: { + if (!device) + return ""; + let s = connected ? qsTr("Connected") : (device.bonded ? qsTr("Paired") : qsTr("Not paired")); + if (connected && device.batteryAvailable) + s += " • " + Math.round(device.battery * 100) + "%"; + return s; + } + + onDeviceChanged: { + // Auto close when device lost + if (!device) + nState.closeSubPage(); + } + + title: device?.name ?? qsTr("Device") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // Big buttons + ButtonRow { + Layout.bottomMargin: Tokens.spacing.large - parent.spacing + Layout.alignment: Qt.AlignHCenter + Layout.minimumWidth: Math.round(root.cappedWidth * 0.7) + spacing: Tokens.spacing.small + + ButtonBase { + id: forgetBtn + + fillWidth: true + shapeMorph: true + isRound: true + + inactiveColour: Colours.palette.m3errorContainer + inactiveOnColour: Colours.palette.m3onErrorContainer + + implicitWidth: forgetBtnLayout.implicitWidth + Tokens.padding.extraLarge * 2 + implicitHeight: forgetBtnLayout.implicitHeight + Tokens.padding.medium * 2 + + onClicked: { + root.device?.forget(); + root.nState.closeSubPage(); + } + + ColumnLayout { + id: forgetBtnLayout + + anchors.centerIn: parent + spacing: 0 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "delete" + color: forgetBtn.onColour + font: Tokens.font.icon.medium + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Forget") + color: forgetBtn.onColour + } + } + } + + ButtonBase { + id: connectBtn + + fillWidth: true + shapeMorph: true + isRound: true + + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer + stateLayer.disabled: root.loading + + implicitWidth: connectBtnContent.implicitWidth + Tokens.padding.extraLarge * 2 + implicitHeight: connectBtnContent.implicitHeight + Tokens.padding.medium * 2 + + onClicked: root.device.connected = !root.connected + + AnimLoader { + id: connectBtnContent + + anchors.centerIn: parent + sourceComp: root.loading ? loadingComp : textComp + outAnimType: Anim.SlowEffects + inAnimType: Anim.SlowEffects + } + + Component { + id: loadingComp + + LoadingIndicator { + implicitSize: connectBtn.height - Tokens.padding.large * 2 + } + } + + Component { + id: textComp + + ColumnLayout { + spacing: 0 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: root.connected ? "close" : "add" + color: connectBtn.inactiveOnColour + font: Tokens.font.icon.medium + animate: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: root.connected ? qsTr("Disconnect") : qsTr("Connect") + color: connectBtn.inactiveOnColour + animate: true + } + } + } + } + } + + // Connection group + ToggleRow { + Layout.fillWidth: true + verticalPadding: Tokens.padding.large + first: true + text: qsTr("Trusted") + subtext: qsTr("Allow this device to connect automatically") + checked: root.device?.trusted ?? false + onToggled: { + if (root.device) + root.device.trusted = checked; + } + } + + ToggleRow { + Layout.fillWidth: true + verticalPadding: Tokens.padding.large + text: qsTr("Blocked") + subtext: qsTr("Prevent this device from connecting") + checked: root.device?.blocked ?? false + onToggled: { + if (root.device) + root.device.blocked = checked; + } + } + + ToggleRow { + Layout.fillWidth: true + verticalPadding: Tokens.padding.large + last: true + text: qsTr("Wake allowed") + subtext: qsTr("Allow this device to wake the system") + checked: root.device?.wakeAllowed ?? false + onToggled: { + if (root.device) + root.device.wakeAllowed = checked; + } + } + + // Information + ConnectedRect { + Layout.topMargin: Tokens.spacing.large - parent.spacing + Layout.fillWidth: true + implicitHeight: batteryLayout.implicitHeight + Tokens.padding.large * 2 + first: true + + ColumnLayout { + id: batteryLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.medium + + StyledText { + Layout.fillWidth: true + text: qsTr("Battery") + } + + StyledText { + text: root.device?.batteryAvailable ? Math.round(root.device.battery * 100) + "%" : qsTr("Unavailable") + color: Colours.palette.m3outline + font: Tokens.font.body.small + } + } + + Loader { + Layout.fillWidth: true + active: root.device?.batteryAvailable ?? false + visible: active + asynchronous: true + + sourceComponent: StyledProgressBar { + implicitHeight: Tokens.padding.medium + value: root.device.battery + } + } + } + } + + ConnectedRect { + Layout.fillWidth: true + implicitHeight: addrLayout.implicitHeight + Tokens.padding.large * 2 + last: true + + RowLayout { + id: addrLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.medium + + StyledText { + Layout.fillWidth: true + text: qsTr("Address") + } + + StyledText { + text: root.device?.address ?? "" + color: Colours.palette.m3outline + font: Tokens.font.body.small + } + } + } + } +} diff --git a/modules/nexus/pages/panels/DashboardPanel.qml b/modules/nexus/pages/panels/DashboardPanel.qml new file mode 100644 index 00000000..64d0292e --- /dev/null +++ b/modules/nexus/pages/panels/DashboardPanel.qml @@ -0,0 +1,146 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Dashboard") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // General + SectionHeader { + first: true + text: qsTr("General") + } + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Enabled") + checked: Config.dashboard.enabled + onToggled: GlobalConfig.dashboard.enabled = checked + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Show on hover") + subtext: qsTr("Reveal when the cursor reaches the screen edge") + checked: Config.dashboard.showOnHover + onToggled: GlobalConfig.dashboard.showOnHover = checked + } + + // Tabs + SectionHeader { + text: qsTr("Tabs") + } + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Dashboard") + checked: Config.dashboard.showDashboard + onToggled: GlobalConfig.dashboard.showDashboard = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Media") + checked: Config.dashboard.showMedia + onToggled: GlobalConfig.dashboard.showMedia = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Performance") + checked: Config.dashboard.showPerformance + onToggled: GlobalConfig.dashboard.showPerformance = checked + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Weather") + checked: Config.dashboard.showWeather + onToggled: GlobalConfig.dashboard.showWeather = checked + } + + // Performance widgets + SectionHeader { + text: qsTr("Performance widgets") + } + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Battery") + checked: Config.dashboard.performance.showBattery + onToggled: GlobalConfig.dashboard.performance.showBattery = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("GPU") + checked: Config.dashboard.performance.showGpu + onToggled: GlobalConfig.dashboard.performance.showGpu = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("CPU") + checked: Config.dashboard.performance.showCpu + onToggled: GlobalConfig.dashboard.performance.showCpu = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Memory") + checked: Config.dashboard.performance.showMemory + onToggled: GlobalConfig.dashboard.performance.showMemory = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Storage") + checked: Config.dashboard.performance.showStorage + onToggled: GlobalConfig.dashboard.performance.showStorage = checked + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Network") + checked: Config.dashboard.performance.showNetwork + onToggled: GlobalConfig.dashboard.performance.showNetwork = checked + } + + // Behaviour + SectionHeader { + text: qsTr("Behaviour") + } + + StepperRow { + Layout.fillWidth: true + first: true + last: true + label: qsTr("Drag threshold") + subtext: qsTr("Pixels dragged before the dashboard opens") + value: Config.dashboard.dragThreshold + from: 0 + to: 200 + stepSize: 5 + onMoved: v => GlobalConfig.dashboard.dragThreshold = v + } + } +} diff --git a/modules/nexus/pages/panels/LauncherPanel.qml b/modules/nexus/pages/panels/LauncherPanel.qml new file mode 100644 index 00000000..ffa2bfcb --- /dev/null +++ b/modules/nexus/pages/panels/LauncherPanel.qml @@ -0,0 +1,147 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Launcher") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // General + SectionHeader { + first: true + text: qsTr("General") + } + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Enabled") + checked: Config.launcher.enabled + onToggled: GlobalConfig.launcher.enabled = checked + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Show on hover") + subtext: qsTr("Reveal when the cursor reaches the screen edge") + checked: Config.launcher.showOnHover + onToggled: GlobalConfig.launcher.showOnHover = checked + } + + // Display + SectionHeader { + text: qsTr("Display") + } + + StepperRow { + Layout.fillWidth: true + first: true + label: qsTr("Max items shown") + value: Config.launcher.maxShown + from: 1 + to: 20 + stepSize: 1 + onMoved: v => GlobalConfig.launcher.maxShown = v + } + + StepperRow { + Layout.fillWidth: true + label: qsTr("Max wallpapers") + value: Config.launcher.maxWallpapers + from: 1 + to: 30 + stepSize: 1 + onMoved: v => GlobalConfig.launcher.maxWallpapers = v + } + + StepperRow { + Layout.fillWidth: true + last: true + label: qsTr("Drag threshold") + subtext: qsTr("Pixels dragged before the launcher opens") + value: Config.launcher.dragThreshold + from: 0 + to: 200 + stepSize: 5 + onMoved: v => GlobalConfig.launcher.dragThreshold = v + } + + // Behaviour + SectionHeader { + text: qsTr("Behaviour") + } + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Vim keybinds") + subtext: qsTr("Navigate results with Ctrl+hjkl") + checked: GlobalConfig.launcher.vimKeybinds + onToggled: GlobalConfig.launcher.vimKeybinds = checked + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Enable dangerous actions") + subtext: qsTr("Allow actions that shut down or log out") + checked: GlobalConfig.launcher.enableDangerousActions + onToggled: GlobalConfig.launcher.enableDangerousActions = checked + } + + // Fuzzy search + SectionHeader { + text: qsTr("Fuzzy search") + } + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Apps") + checked: GlobalConfig.launcher.useFuzzy.apps + onToggled: GlobalConfig.launcher.useFuzzy.apps = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Actions") + checked: GlobalConfig.launcher.useFuzzy.actions + onToggled: GlobalConfig.launcher.useFuzzy.actions = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Schemes") + checked: GlobalConfig.launcher.useFuzzy.schemes + onToggled: GlobalConfig.launcher.useFuzzy.schemes = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Variants") + checked: GlobalConfig.launcher.useFuzzy.variants + onToggled: GlobalConfig.launcher.useFuzzy.variants = checked + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Wallpapers") + checked: GlobalConfig.launcher.useFuzzy.wallpapers + onToggled: GlobalConfig.launcher.useFuzzy.wallpapers = checked + } + } +} diff --git a/modules/nexus/pages/panels/SidebarPanel.qml b/modules/nexus/pages/panels/SidebarPanel.qml new file mode 100644 index 00000000..0ceaad69 --- /dev/null +++ b/modules/nexus/pages/panels/SidebarPanel.qml @@ -0,0 +1,46 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Sidebar") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + SectionHeader { + first: true + text: qsTr("General") + } + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Enabled") + checked: Config.sidebar.enabled + onToggled: GlobalConfig.sidebar.enabled = checked + } + + StepperRow { + Layout.fillWidth: true + last: true + label: qsTr("Drag threshold") + subtext: qsTr("Pixels dragged before the sidebar opens") + value: Config.sidebar.dragThreshold + from: 0 + to: 200 + stepSize: 5 + onMoved: v => GlobalConfig.sidebar.dragThreshold = v + } + } +} diff --git a/modules/nexus/pages/panels/TaskbarPanel.qml b/modules/nexus/pages/panels/TaskbarPanel.qml new file mode 100644 index 00000000..5b0bb870 --- /dev/null +++ b/modules/nexus/pages/panels/TaskbarPanel.qml @@ -0,0 +1,127 @@ +pragma ComponentBehavior: Bound + +import QtQuick.Layouts +import Caelestia.Config +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Taskbar") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // Behaviour + SectionHeader { + first: true + text: qsTr("Behaviour") + } + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Persistent") + subtext: qsTr("Keep the bar visible at all times") + checked: Config.bar.persistent + onToggled: GlobalConfig.bar.persistent = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Show on hover") + subtext: qsTr("Reveal the bar when the cursor reaches the screen edge") + checked: Config.bar.showOnHover + onToggled: GlobalConfig.bar.showOnHover = checked + } + + StepperRow { + Layout.fillWidth: true + last: true + label: qsTr("Drag threshold") + subtext: qsTr("Pixels dragged before the bar reveals") + value: Config.bar.dragThreshold + from: 0 + to: 200 + stepSize: 5 + onMoved: v => GlobalConfig.bar.dragThreshold = v + } + + // Components + SectionHeader { + text: qsTr("Components") + } + + NavRow { + first: true + icon: "workspaces" + label: qsTr("Workspaces") + status: qsTr("Indicators, window icons") + onClicked: root.nState.openSubPage(5) + } + + NavRow { + icon: "web_asset" + label: qsTr("Active window") + status: qsTr("Title display, popout") + onClicked: root.nState.openSubPage(6) + } + + NavRow { + icon: "widgets" + label: qsTr("Tray") + status: qsTr("System tray icons") + onClicked: root.nState.openSubPage(7) + } + + NavRow { + icon: "signal_cellular_alt" + label: qsTr("Status icons") + status: qsTr("Visible indicators") + onClicked: root.nState.openSubPage(8) + } + + NavRow { + last: true + icon: "schedule" + label: qsTr("Clock") + status: qsTr("Date, icon, background") + onClicked: root.nState.openSubPage(9) + } + + // Scroll actions + SectionHeader { + text: qsTr("Scroll actions") + } + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Workspaces") + subtext: qsTr("Scroll over the workspace indicator to switch workspaces") + checked: Config.bar.scrollActions.workspaces + onToggled: GlobalConfig.bar.scrollActions.workspaces = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Volume") + subtext: qsTr("Scroll on the top half of the bar to adjust volume") + checked: Config.bar.scrollActions.volume + onToggled: GlobalConfig.bar.scrollActions.volume = checked + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Brightness") + subtext: qsTr("Scroll on the bottom half of the bar to adjust brightness") + checked: Config.bar.scrollActions.brightness + onToggled: GlobalConfig.bar.scrollActions.brightness = checked + } + } +} diff --git a/modules/nexus/pages/panels/taskbar/BarActiveWindow.qml b/modules/nexus/pages/panels/taskbar/BarActiveWindow.qml new file mode 100644 index 00000000..e61685e1 --- /dev/null +++ b/modules/nexus/pages/panels/taskbar/BarActiveWindow.qml @@ -0,0 +1,51 @@ +pragma ComponentBehavior: Bound + +import QtQuick.Layouts +import Caelestia.Config +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Active window") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Compact") + checked: Config.bar.activeWindow.compact + onToggled: GlobalConfig.bar.activeWindow.compact = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Inverted") + checked: Config.bar.activeWindow.inverted + onToggled: GlobalConfig.bar.activeWindow.inverted = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Show on hover") + subtext: qsTr("Only show the active window title while hovering") + checked: Config.bar.activeWindow.showOnHover + onToggled: GlobalConfig.bar.activeWindow.showOnHover = checked + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Popout on hover") + subtext: qsTr("Show a window details popout when hovering") + checked: Config.bar.popouts.activeWindow + onToggled: GlobalConfig.bar.popouts.activeWindow = checked + } + } +} diff --git a/modules/nexus/pages/panels/taskbar/BarClock.qml b/modules/nexus/pages/panels/taskbar/BarClock.qml new file mode 100644 index 00000000..b4291b00 --- /dev/null +++ b/modules/nexus/pages/panels/taskbar/BarClock.qml @@ -0,0 +1,42 @@ +pragma ComponentBehavior: Bound + +import QtQuick.Layouts +import Caelestia.Config +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Clock") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Background") + checked: Config.bar.clock.background + onToggled: GlobalConfig.bar.clock.background = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Show date") + checked: Config.bar.clock.showDate + onToggled: GlobalConfig.bar.clock.showDate = checked + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Show icon") + checked: Config.bar.clock.showIcon + onToggled: GlobalConfig.bar.clock.showIcon = checked + } + } +} diff --git a/modules/nexus/pages/panels/taskbar/BarStatusIcons.qml b/modules/nexus/pages/panels/taskbar/BarStatusIcons.qml new file mode 100644 index 00000000..0b9c2631 --- /dev/null +++ b/modules/nexus/pages/panels/taskbar/BarStatusIcons.qml @@ -0,0 +1,98 @@ +pragma ComponentBehavior: Bound + +import QtQuick.Layouts +import Caelestia.Config +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Status icons") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // Visible icons + SectionHeader { + first: true + text: qsTr("Visible icons") + } + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Speakers") + checked: Config.bar.status.showAudio + onToggled: GlobalConfig.bar.status.showAudio = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Microphone") + checked: Config.bar.status.showMicrophone + onToggled: GlobalConfig.bar.status.showMicrophone = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Keyboard layout") + checked: Config.bar.status.showKbLayout + onToggled: GlobalConfig.bar.status.showKbLayout = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Network") + checked: Config.bar.status.showNetwork + onToggled: GlobalConfig.bar.status.showNetwork = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Wi-Fi") + checked: Config.bar.status.showWifi + onToggled: GlobalConfig.bar.status.showWifi = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Bluetooth") + checked: Config.bar.status.showBluetooth + onToggled: GlobalConfig.bar.status.showBluetooth = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Battery") + checked: Config.bar.status.showBattery + onToggled: GlobalConfig.bar.status.showBattery = checked + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Caps lock") + checked: Config.bar.status.showLockStatus + onToggled: GlobalConfig.bar.status.showLockStatus = checked + } + + // Behaviour + SectionHeader { + text: qsTr("Behaviour") + } + + ToggleRow { + Layout.fillWidth: true + first: true + last: true + text: qsTr("Popout on hover") + subtext: qsTr("Show a details popout when hovering the status icons") + checked: Config.bar.popouts.statusIcons + onToggled: GlobalConfig.bar.popouts.statusIcons = checked + } + } +} diff --git a/modules/nexus/pages/panels/taskbar/BarTray.qml b/modules/nexus/pages/panels/taskbar/BarTray.qml new file mode 100644 index 00000000..3d00196e --- /dev/null +++ b/modules/nexus/pages/panels/taskbar/BarTray.qml @@ -0,0 +1,50 @@ +pragma ComponentBehavior: Bound + +import QtQuick.Layouts +import Caelestia.Config +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Tray") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + ToggleRow { + Layout.fillWidth: true + first: true + text: qsTr("Background") + checked: Config.bar.tray.background + onToggled: GlobalConfig.bar.tray.background = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Recolour icons") + checked: Config.bar.tray.recolour + onToggled: GlobalConfig.bar.tray.recolour = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Compact") + checked: Config.bar.tray.compact + onToggled: GlobalConfig.bar.tray.compact = checked + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Popout on hover") + subtext: qsTr("Show the tray menu popout when hovering") + checked: Config.bar.popouts.tray + onToggled: GlobalConfig.bar.popouts.tray = checked + } + } +} diff --git a/modules/nexus/pages/panels/taskbar/BarWorkspaces.qml b/modules/nexus/pages/panels/taskbar/BarWorkspaces.qml new file mode 100644 index 00000000..d1388f58 --- /dev/null +++ b/modules/nexus/pages/panels/taskbar/BarWorkspaces.qml @@ -0,0 +1,86 @@ +pragma ComponentBehavior: Bound + +import QtQuick.Layouts +import Caelestia.Config +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Workspaces") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + StepperRow { + Layout.fillWidth: true + first: true + label: qsTr("Shown") + subtext: qsTr("Number of workspaces displayed") + value: Config.bar.workspaces.shown + from: 1 + to: 20 + stepSize: 1 + onMoved: v => GlobalConfig.bar.workspaces.shown = v + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Active indicator") + checked: Config.bar.workspaces.activeIndicator + onToggled: GlobalConfig.bar.workspaces.activeIndicator = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Active trail") + checked: Config.bar.workspaces.activeTrail + onToggled: GlobalConfig.bar.workspaces.activeTrail = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Occupied background") + checked: Config.bar.workspaces.occupiedBg + onToggled: GlobalConfig.bar.workspaces.occupiedBg = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Show windows") + subtext: qsTr("Show icons of open windows on each workspace") + checked: Config.bar.workspaces.showWindows + onToggled: GlobalConfig.bar.workspaces.showWindows = checked + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Windows on special workspaces") + checked: Config.bar.workspaces.showWindowsOnSpecialWorkspaces + onToggled: GlobalConfig.bar.workspaces.showWindowsOnSpecialWorkspaces = checked + } + + StepperRow { + Layout.fillWidth: true + label: qsTr("Max window icons") + value: Config.bar.workspaces.maxWindowIcons + from: 0 + to: 20 + stepSize: 1 + onMoved: v => GlobalConfig.bar.workspaces.maxWindowIcons = v + } + + ToggleRow { + Layout.fillWidth: true + last: true + text: qsTr("Per-monitor workspaces") + subtext: qsTr("Show each monitor's workspaces independently") + checked: GlobalConfig.bar.workspaces.perMonitorWorkspaces + onToggled: GlobalConfig.bar.workspaces.perMonitorWorkspaces = checked + } + } +} diff --git a/modules/nexus/pages/wallandstyle/ColourSelect.qml b/modules/nexus/pages/wallandstyle/ColourSelect.qml new file mode 100644 index 00000000..e7816d0b --- /dev/null +++ b/modules/nexus/pages/wallandstyle/ColourSelect.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Colours") + isSubPage: true + + Item { + anchors.horizontalCenter: parent.horizontalCenter + implicitHeight: { + const f = parent.parent as Flickable; + return f.height - f.topMargin - f.bottomMargin; + } + + ColumnLayout { + anchors.centerIn: parent + spacing: Tokens.padding.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "handyman" + color: Colours.palette.m3outlineVariant + font: Tokens.font.icon.extraLarge + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Page under construction") + color: Colours.palette.m3outlineVariant + font: Tokens.font.title.large + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("This page will be available in a future update.") + color: Colours.palette.m3outlineVariant + font: Tokens.font.body.large + } + } + } +} diff --git a/modules/nexus/pages/wallandstyle/WallpaperCategory.qml b/modules/nexus/pages/wallandstyle/WallpaperCategory.qml new file mode 100644 index 00000000..7b35fb3a --- /dev/null +++ b/modules/nexus/pages/wallandstyle/WallpaperCategory.qml @@ -0,0 +1,53 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import Caelestia.Models +import qs.services +import qs.modules.nexus.common + +PageBase { + id: root + + title: { + const c = nState.selectedWallpaperCategory; + return c.slice(0, 1).toUpperCase() + c.slice(1); + } + isSubPage: true + + GridLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + + columns: Config.nexus.wallpapersPerRow + rowSpacing: Tokens.spacing.medium + columnSpacing: Tokens.spacing.large + + Repeater { + model: { + const walls = Wallpapers.list.filter(w => Wallpapers.getCategoryFor(w) === root.nState.selectedWallpaperCategory).sort((a, b) => a.name.localeCompare(b.name)); + while (walls.length < Config.nexus.wallpapersPerRow) + walls.push(null); + return walls; + } + + WallItem { + required property FileSystemEntry modelData + + // Empty placeholders for sizing + opacity: modelData ? 1 : 0 + enabled: modelData + + source: String(modelData?.path ?? "") + text: modelData?.name ?? "" + onClicked: { + Wallpapers.setWallpaper(modelData.path); + root.nState.closeSubPage(); + root.nState.closeSubPage(); + } + } + } + } +} diff --git a/modules/nexus/pages/wallandstyle/WallpaperSelect.qml b/modules/nexus/pages/wallandstyle/WallpaperSelect.qml new file mode 100644 index 00000000..83e0d73a --- /dev/null +++ b/modules/nexus/pages/wallandstyle/WallpaperSelect.qml @@ -0,0 +1,184 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Components +import Caelestia.Config +import Caelestia.Models +import qs.components +import qs.components.controls +import qs.components.filedialog +import qs.services +import qs.utils +import qs.modules.nexus.common + +PageBase { + id: root + + title: qsTr("Wallpapers") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.small + + ButtonRow { + Layout.bottomMargin: Tokens.spacing.medium + Layout.alignment: Qt.AlignHCenter + spacing: Tokens.spacing.small + + IconTextButton { + icon: "photo_library" + text: qsTr("Browse") + font: Tokens.font.body.large + isRound: true + shapeMorph: true + horizontalPadding: Tokens.padding.extraLarge + verticalPadding: Tokens.padding.medium + onClicked: browseDialog.open() + + FileDialog { + id: browseDialog + + title: qsTr("Select an image") + filterLabel: qsTr("Image files") + filters: Images.validImageExtensions + onAccepted: path => { + Wallpapers.setWallpaper(path); + root.nState.closeSubPage(); + } + } + } + + IconTextButton { + icon: "shuffle" + text: qsTr("Random") + font: Tokens.font.body.large + isRound: true + shapeMorph: true + horizontalPadding: Tokens.padding.extraLarge + verticalPadding: Tokens.padding.medium + type: IconTextButton.Tonal + onClicked: { + Wallpapers.setRandom(); + root.nState.closeSubPage(); + } + } + } + + WallItem { + imgHeight: Math.round(width * 0.3) + radius: Tokens.rounding.extraLarge + source: Quickshell.shellPath("assets/wallpaper.webp") + text: qsTr("Featured wallpaper") + fillLabel: false + onClicked: { + Wallpapers.setWallpaper(Quickshell.shellPath("assets/wallpaper.webp")); + root.nState.closeSubPage(); + } + } + + StyledText { + Layout.topMargin: Tokens.spacing.large + text: qsTr("Local wallpapers") + font: Tokens.font.title.small + } + + GridLayout { + Layout.fillWidth: true + visible: localWalls.count > 0 + + columns: Config.nexus.wallpapersPerRow + rowSpacing: Tokens.spacing.medium + columnSpacing: Tokens.spacing.large + + Repeater { + id: localWalls + + model: { + const walls = Wallpapers.list; + const baseDir = Paths.wallsdir; + const categories = {}; + for (const w of walls) { + if (w.parentDir !== baseDir) { + const category = Wallpapers.getCategoryFor(w); + if (category && (!(category in categories) || categories[category].name.localeCompare(w.name) > 0)) + categories[category] = w; + } + } + const cats = Object.values(categories); + while (cats.length < Config.nexus.wallpapersPerRow) + cats.push(null); + return cats; + } + + WallItem { + required property FileSystemEntry modelData + + // Empty placeholders for sizing + opacity: modelData ? 1 : 0 + enabled: modelData + + source: String(modelData?.path ?? "") + text: { + if (!modelData) + return ""; + + if (modelData.parentDir !== Paths.wallsdir) { + const category = Wallpapers.getCategoryFor(modelData); + return category.slice(0, 1).toUpperCase() + category.slice(1); + } + return modelData.name; + } + onClicked: { + if (modelData.parentDir !== Paths.wallsdir) { + root.nState.selectedWallpaperCategory = Wallpapers.getCategoryFor(modelData); + root.nState.openSubPage(2); // Category page + } else { + Wallpapers.setWallpaper(modelData.path); + root.nState.closeSubPage(); + } + } + } + } + } + + Loader { + Layout.fillWidth: true + + asynchronous: true + active: localWalls.count === 0 + visible: active + + sourceComponent: StyledRect { + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.extraLarge + implicitHeight: noWallsLayout.implicitHeight + Tokens.padding.extraExtraLarge * 2 + + ColumnLayout { + id: noWallsLayout + + anchors.centerIn: parent + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "hide_image" + color: Colours.palette.m3outline + fontStyle: Tokens.font.icon.extraLarge + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("No local wallpapers found") + color: Colours.palette.m3outline + font: Tokens.font.title.small + } + } + } + } + } +} diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index aeaa7dea..df8418c4 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -1,11 +1,15 @@ +pragma ComponentBehavior: Bound + import QtQuick import Quickshell import Quickshell.Widgets +import Caelestia import Caelestia.Config import qs.components import qs.components.containers import qs.components.widgets import qs.services +import qs.modules.utilities as Utilities Item { id: root @@ -13,43 +17,53 @@ Item { required property DrawerVisibilities visibilities required property Item osdPanel required property Item sessionPanel + required property Item utilitiesPanel readonly property int padding: Tokens.padding.large + readonly property int clampedPadding: CUtils.clamp(padding - Config.border.thickness, 0, padding) anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right - implicitWidth: Tokens.sizes.notifs.width + padding * 2 + implicitWidth: Tokens.sizes.notifs.width implicitHeight: { const count = list.count; if (count === 0) return 0; - let height = (count - 1) * Tokens.spacing.smaller; + let height = (count - 1) * Tokens.spacing.medium; for (let i = 0; i < count; i++) height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; if (visibilities.osd) { - const h = osdPanel.y - Config.border.rounding * 2 - padding * 2; + const h = osdPanel.y - clampedPadding; if (height > h) height = h; } if (visibilities.session) { - const h = sessionPanel.y - Config.border.rounding * 2 - padding * 2; + const h = sessionPanel.y - clampedPadding; if (height > h) height = h; } - return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); + if (visibilities.utilities) { + const h = ((QsWindow.window as QsWindow)?.screen.height ?? 0) - (utilitiesPanel as Utilities.Wrapper).nonAnimHeight - Config.border.thickness * 2 - padding * 2 - Tokens.spacing.extraLarge; + if (height > h) + height = h; + } + + return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) + padding - clampedPadding * 2 - Config.border.thickness, height + padding + clampedPadding); } ClippingWrapperRectangle { anchors.fill: parent anchors.margins: root.padding + anchors.topMargin: root.clampedPadding + anchors.rightMargin: root.clampedPadding color: "transparent" - radius: Tokens.rounding.normal + radius: Tokens.rounding.large StyledListView { id: list @@ -89,9 +103,9 @@ Item { let height = 0; for (let i = 0; i < count; i++) { - height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; + height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.medium; - if (height - Tokens.spacing.smaller >= scrollY) + if (height - Tokens.spacing.medium >= scrollY) return i; } @@ -110,9 +124,9 @@ Item { let height = 0; for (let i = count - 1; i >= 0; i--) { - height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; + height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.medium; - if (height - Tokens.spacing.smaller >= scrollY) + if (height - Tokens.spacing.medium >= scrollY) return count - i - 1; } @@ -140,7 +154,7 @@ Item { } implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.smaller) + implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.medium) ListView.onRemove: removeAnim.start() @@ -170,7 +184,7 @@ Item { Anim { target: notif property: "x" - to: (notif.x >= 0 ? wrapper.Tokens.sizes.notifs.width : -wrapper.Tokens.sizes.notifs.width) * 2 + to: (notif.x >= 0 ? root.implicitWidth : -root.implicitWidth) * 2 duration: Tokens.anim.durations.normal easing: Tokens.anim.emphasized } @@ -183,7 +197,7 @@ Item { ClippingRectangle { anchors.top: parent.top - anchors.topMargin: wrapper.idx === 0 ? 0 : Tokens.spacing.smaller + anchors.topMargin: wrapper.idx === 0 ? 0 : Tokens.spacing.medium color: "transparent" radius: notif.radius @@ -194,6 +208,7 @@ Item { id: notif modelData: wrapper.modelData + implicitWidth: root.implicitWidth - root.padding - root.clampedPadding } } } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index fe6fd056..5a10b8e7 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -1,12 +1,13 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Layouts import QtQuick.Shapes import Quickshell import Quickshell.Services.Notifications +import Caelestia.Components import Caelestia.Config import qs.components +import qs.components.controls import qs.components.effects import qs.services import qs.utils @@ -18,15 +19,15 @@ StyledRect { readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText - readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 + readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? Tokens.spacing.extraSmall * 2 + appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 property bool expanded: Config.notifs.openExpanded color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.normal - implicitWidth: Tokens.sizes.notifs.width + radius: Tokens.rounding.large + implicitHeight: inner.implicitHeight - x: Tokens.sizes.notifs.width + x: implicitWidth Component.onCompleted: { x = 0; modelData.lock(this); @@ -67,7 +68,7 @@ StyledRect { if (!containsMouse) root.modelData.timer.start(); - if (Math.abs(root.x) < Tokens.sizes.notifs.width * Config.notifs.clearThreshold) + if (Math.abs(root.x) < root.implicitWidth * Config.notifs.clearThreshold) root.x = 0; else root.modelData.popup = false; @@ -94,14 +95,12 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium implicitHeight: root.nonAnimHeight Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { @@ -176,14 +175,12 @@ StyledRect { asynchronous: true active: !root.hasAppIcon anchors.centerIn: parent - anchors.horizontalCenterOffset: -Tokens.font.size.large * 0.02 - anchors.verticalCenterOffset: Tokens.font.size.large * 0.02 + anchors.verticalCenterOffset: 1 sourceComponent: MaterialIcon { text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.medium } } } @@ -208,9 +205,9 @@ StyledRect { PathAngleArc { id: progressArc - radiusX: progressIndicator.width / 2 - root.Tokens.padding.small / 2 + radiusX: progressIndicator.width / 2 - root.Tokens.padding.extraSmall / 2 centerX: progressIndicator.width / 2 - radiusY: progressIndicator.height / 2 - root.Tokens.padding.small / 2 + radiusY: progressIndicator.height / 2 - root.Tokens.padding.extraSmall / 2 centerY: progressIndicator.height / 2 startAngle: -90 @@ -230,18 +227,20 @@ StyledRect { anchors.top: parent.top anchors.left: image.right - anchors.leftMargin: Tokens.spacing.smaller + anchors.leftMargin: Tokens.spacing.medium animate: true text: appNameMetrics.elidedText maximumLineCount: 1 color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.label.medium opacity: root.expanded ? 1 : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -249,8 +248,7 @@ StyledRect { id: appNameMetrics text: root.modelData.appName - font.family: appName.font.family - font.pointSize: appName.font.pointSize + font: appName.font elide: Text.ElideRight elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 } @@ -260,7 +258,7 @@ StyledRect { anchors.top: parent.top anchors.left: image.right - anchors.leftMargin: Tokens.spacing.smaller + anchors.leftMargin: Tokens.spacing.medium animate: true text: summaryMetrics.elidedText @@ -273,6 +271,9 @@ StyledRect { PropertyChanges { summary.maximumLineCount: undefined + summary.anchors.topMargin: root.Tokens.spacing.extraSmall + bodyPreview.anchors.topMargin: root.Tokens.spacing.extraSmall + body.anchors.topMargin: root.Tokens.spacing.extraSmall } AnchorChanges { @@ -286,9 +287,10 @@ StyledRect { target: summary property: "maximumLineCount" } - AnchorAnim { - type: AnchorAnim.Standard + Anim { + property: "topMargin" } + AnchorAnim {} } Behavior on height { @@ -300,8 +302,7 @@ StyledRect { id: summaryMetrics text: root.modelData.summary - font.family: summary.font.family - font.pointSize: summary.font.pointSize + font: summary.font elide: Text.ElideRight elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 } @@ -315,7 +316,7 @@ StyledRect { text: "•" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small states: State { name: "expanded" @@ -328,9 +329,7 @@ StyledRect { } transitions: Transition { - AnchorAnim { - type: AnchorAnim.Standard - } + AnchorAnim {} } } @@ -345,7 +344,7 @@ StyledRect { horizontalAlignment: Text.AlignLeft text: root.modelData.timeStr color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } Item { @@ -353,9 +352,10 @@ StyledRect { anchors.right: parent.right anchors.top: parent.top + anchors.topMargin: -Tokens.padding.extraSmall - implicitWidth: expandIcon.height - implicitHeight: expandIcon.height + implicitWidth: expandIcon.implicitHeight + implicitHeight: expandIcon.implicitHeight StateLayer { radius: Tokens.rounding.full @@ -367,10 +367,18 @@ StyledRect { id: expandIcon anchors.centerIn: parent + anchors.verticalCenterOffset: root.expanded ? -1 : 1 + text: "expand_more" + fontStyle: Tokens.font.icon.medium + rotation: root.expanded ? 180 : 0 - animate: true - text: root.expanded ? "expand_less" : "expand_more" - font.pointSize: Tokens.font.size.normal + Behavior on anchors.verticalCenterOffset { + Anim {} + } + + Behavior on rotation { + Anim {} + } } } @@ -386,12 +394,14 @@ StyledRect { textFormat: root.bodyTextFormat text: bodyPreviewMetrics.elidedText color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small opacity: root.expanded ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -399,8 +409,7 @@ StyledRect { id: bodyPreviewMetrics text: root.modelData.body - font.family: bodyPreview.font.family - font.pointSize: bodyPreview.font.pointSize + font: bodyPreview.font elide: Text.ElideRight elideWidth: bodyPreview.width } @@ -417,7 +426,7 @@ StyledRect { textFormat: root.bodyTextFormat text: root.modelData.body color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small wrapMode: Text.WrapAtWordBoundaryOrAnywhere height: text ? implicitHeight : 0 @@ -432,85 +441,85 @@ StyledRect { opacity: root.expanded ? 1 : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } - RowLayout { + ButtonRow { id: actions - anchors.horizontalCenter: parent.horizontalCenter + anchors.left: body.left + anchors.right: body.right anchors.top: body.bottom anchors.topMargin: Tokens.spacing.small - spacing: Tokens.spacing.smaller - + spacing: Tokens.spacing.extraSmall opacity: root.expanded ? 1 : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } - Action { - modelData: QtObject { - readonly property string text: qsTr("Close") - - function invoke(): void { - root.modelData.close(); - } - } + IconButton { + isRound: true + shapeMorph: true + fillWidth: root.modelData.actions.length === 0 + inactiveColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + inactiveOnColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant + icon: "close" + padding: Tokens.padding.extraSmall + onClicked: root.modelData.close() } Repeater { model: root.modelData.actions - delegate: Component { - Action {} + TextButton { + required property var modelData + + isRound: true + shapeMorph: true + fillWidth: true + inactiveColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + inactiveOnColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant + text: modelData.text + onClicked: modelData.invoke() + + label.horizontalAlignment: Text.AlignHCenter + label.anchors.left: left + label.anchors.right: right + label.anchors.verticalCenter: verticalCenter + label.anchors.centerIn: undefined + label.anchors.margins: Tokens.padding.medium + label.elide: Text.ElideRight + } + } + + IconButton { + isRound: true + shapeMorph: true + fillWidth: root.modelData.actions.length === 0 + inactiveColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + inactiveOnColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant + icon: copyTimer.running ? "inventory" : "content_copy" + padding: Tokens.padding.extraSmall + onClicked: { + Quickshell.clipboardText = root.modelData.body; + copyTimer.restart(); + } + label.animate: true + + Timer { + id: copyTimer + + interval: 3000 } } } } } - - component Action: StyledRect { - id: action - - required property var modelData - - radius: Tokens.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - - Layout.preferredWidth: actionText.width + Tokens.padding.normal * 2 - Layout.preferredHeight: actionText.height + Tokens.padding.small * 2 - implicitWidth: actionText.width + Tokens.padding.normal * 2 - implicitHeight: actionText.height + Tokens.padding.small * 2 - - StateLayer { - radius: Tokens.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface - onClicked: action.modelData.invoke() - } - - StyledText { - id: actionText - - anchors.centerIn: parent - text: actionTextMetrics.elidedText - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - } - - TextMetrics { - id: actionTextMetrics - - text: action.modelData.text - font.family: actionText.font.family - font.pointSize: actionText.font.pointSize - elide: Text.ElideRight - elideWidth: { - const numActions = root.modelData.actions.length + 1; - return (inner.width - actions.spacing * (numActions - 1)) / numActions - root.Tokens.padding.normal * 2; - } - } - } } diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 97417e9c..2c6ebea6 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -8,6 +8,7 @@ Item { required property Item sidebarPanel property alias osdPanel: content.osdPanel property alias sessionPanel: content.sessionPanel + property alias utilitiesPanel: content.utilitiesPanel visible: height > 0 anchors.topMargin: -5 @@ -17,6 +18,7 @@ Item { Content { id: content + anchors.topMargin: -root.anchors.topMargin visibilities: root.visibilities } } diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 3ad3ef88..1aac8558 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Caelestia import Caelestia.Config import qs.components import qs.components.controls @@ -20,14 +21,15 @@ Item { required property bool sourceMuted required property real brightness - implicitWidth: layout.implicitWidth + Tokens.padding.large * 2 + implicitWidth: layout.implicitWidth + Tokens.padding.large + layout.anchors.horizontalCenterOffset * 2 implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 ColumnLayout { id: layout anchors.centerIn: parent - spacing: Tokens.spacing.normal + anchors.horizontalCenterOffset: CUtils.clamp(Tokens.padding.large - Config.border.thickness, 0, Tokens.padding.large) / 2 + spacing: Tokens.spacing.medium // Speaker volume CustomMouseArea { @@ -122,7 +124,9 @@ Item { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 3ea0e1a3..b6f058b8 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -45,9 +45,7 @@ Item { opacity: 1 - offsetScale Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Connections { diff --git a/modules/session/Content.qml b/modules/session/Content.qml index e1ba2809..f70e6493 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -2,8 +2,10 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Caelestia import Caelestia.Config import qs.components +import qs.components.controls import qs.services import qs.utils @@ -13,6 +15,7 @@ Column { required property DrawerVisibilities visibilities padding: Tokens.padding.large + rightPadding: CUtils.clamp(padding - Config.border.thickness, 0, padding) spacing: Tokens.spacing.large SessionButton { @@ -76,17 +79,19 @@ Column { KeyNavigation.up: hibernate } - component SessionButton: StyledRect { + component SessionButton: IconButton { id: button - required property string icon required property list command implicitWidth: Tokens.sizes.session.button implicitHeight: Tokens.sizes.session.button - radius: Tokens.rounding.large - color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer + inactiveColour: activeFocus ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer + inactiveOnColour: activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + radius: pressed ? Tokens.rounding.medium : activeFocus ? Tokens.rounding.extraLarge : Tokens.rounding.largeIncreased + font: Tokens.font.icon.builders.large.scale(1.3).build() + onClicked: Quickshell.execDetached(button.command) Keys.onEnterPressed: Quickshell.execDetached(button.command) Keys.onReturnPressed: Quickshell.execDetached(button.command) @@ -113,20 +118,5 @@ Column { } } } - - StateLayer { - radius: parent.radius - color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - onClicked: Quickshell.execDetached(button.command) - } - - MaterialIcon { - anchors.centerIn: parent - - text: button.icon - color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 - } } } diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 41d9ba1a..aab2118c 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -22,9 +22,7 @@ Item { opacity: 1 - offsetScale Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml index f6b8eb05..f5cde346 100644 --- a/modules/sidebar/Content.qml +++ b/modules/sidebar/Content.qml @@ -14,13 +14,13 @@ Item { id: layout anchors.fill: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledRect { Layout.fillWidth: true Layout.fillHeight: true - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainerLow NotifDock { diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 2a390926..a14f5aa1 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -16,11 +16,11 @@ StyledRect { required property DrawerVisibilities visibilities readonly property StyledText body: (expandedContent.item as ExpandedBody)?.body ?? null - readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.normal * 2 : summaryHeightMetrics.height + readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.medium * 2 : summaryHeightMetrics.height implicitHeight: nonAnimHeight - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: { const c = root.modelData?.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); return expanded ? c : Qt.alpha(c, 0); @@ -32,12 +32,12 @@ StyledRect { name: "expanded" PropertyChanges { - summary.anchors.margins: root.Tokens.padding.normal - dummySummary.anchors.margins: root.Tokens.padding.normal - compactBody.anchors.margins: root.Tokens.padding.normal - timeStr.anchors.margins: root.Tokens.padding.normal - expandedContent.anchors.margins: root.Tokens.padding.normal - summary.width: root.width - root.Tokens.padding.normal * 2 - timeStr.implicitWidth - root.Tokens.spacing.small + summary.anchors.margins: root.Tokens.padding.medium + dummySummary.anchors.margins: root.Tokens.padding.medium + compactBody.anchors.margins: root.Tokens.padding.medium + timeStr.anchors.margins: root.Tokens.padding.medium + expandedContent.anchors.margins: root.Tokens.padding.medium + summary.width: root.width - root.Tokens.padding.medium * 2 - timeStr.implicitWidth - root.Tokens.spacing.small summary.maximumLineCount: Number.MAX_SAFE_INTEGER } } @@ -106,7 +106,7 @@ StyledRect { animate: true text: root.modelData?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } } @@ -117,21 +117,19 @@ StyledRect { anchors.top: summary.bottom anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: Tokens.spacing.small / 2 + anchors.topMargin: Tokens.spacing.extraSmall sourceComponent: ExpandedBody {} } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } component ExpandedBody: ColumnLayout { readonly property alias body: bodyText - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { id: bodyText @@ -182,6 +180,7 @@ StyledRect { property: "active" } Anim { + type: Anim.DefaultEffects property: "opacity" } } @@ -192,6 +191,7 @@ StyledRect { SequentialAnimation { Anim { + type: Anim.DefaultEffects property: "opacity" } PropertyAction { diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index 084fe678..ed787b66 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -20,7 +20,7 @@ Item { layer.enabled: true layer.smooth: true - layer.effect: OpacityMask { + layer.effect: Mask { maskSource: gradientMask } @@ -65,7 +65,9 @@ Item { opacity: flickable.contentX > 0 ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -78,7 +80,9 @@ Item { opacity: flickable.contentX < flickable.contentWidth - parent.width ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -114,11 +118,11 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - implicitWidth: actionInner.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: actionInner.implicitHeight + Tokens.padding.small * 2 + implicitWidth: actionInner.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: actionInner.implicitHeight + Tokens.padding.small Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Tokens.padding.large : 0) - radius: actionStateLayer.pressed ? Tokens.rounding.small / 2 : Tokens.rounding.small + radius: actionStateLayer.pressed ? Tokens.rounding.medium / 2 : Tokens.rounding.medium color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 4) Timer { diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index 4509ce26..10014a43 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -20,7 +20,7 @@ Item { readonly property int notifCount: Notifs.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium Component.onCompleted: Notifs.list.forEach(n => n.popup = false) @@ -30,7 +30,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Tokens.padding.small + anchors.margins: Tokens.padding.extraSmall implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight) @@ -44,16 +44,16 @@ Item { text: root.notifCount color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.label.large Behavior on anchors.leftMargin { Anim {} } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -63,13 +63,11 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: count.right anchors.right: parent.right - anchors.leftMargin: Tokens.spacing.small + anchors.leftMargin: Tokens.spacing.extraSmall text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.label.large elide: Text.ElideRight } } @@ -81,9 +79,9 @@ Item { anchors.right: parent.right anchors.top: title.bottom anchors.bottom: parent.bottom - anchors.topMargin: Tokens.spacing.smaller + anchors.topMargin: Tokens.spacing.medium - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: "transparent" Loader { @@ -93,7 +91,7 @@ Item { opacity: root.notifCount > 0 ? 0 : 1 sourceComponent: ColumnLayout { - spacing: Tokens.spacing.large + spacing: Tokens.spacing.extraLarge Image { asynchronous: true @@ -110,11 +108,9 @@ Item { StyledText { Layout.alignment: Qt.AlignHCenter - text: qsTr("No Notifications") + text: qsTr("All up to date!") color: Colours.palette.m3outlineVariant - font.pointSize: Tokens.font.size.large - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.headline.builders.small.width(90).build() } } @@ -178,7 +174,7 @@ Item { asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium scale: root.notifCount > 0 ? 1 : 0.5 opacity: root.notifCount > 0 ? 1 : 0 @@ -188,9 +184,7 @@ Item { id: clearBtn icon: "clear_all" - radius: Tokens.rounding.normal - padding: Tokens.padding.normal - font.pointSize: Math.round(Tokens.font.size.large * 1.2) + font: Tokens.font.icon.large onClicked: clearTimer.start() Elevation { @@ -209,7 +203,7 @@ Item { Behavior on opacity { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial + type: Anim.DefaultEffects } } } diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 2677698b..aa9a962a 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -128,25 +128,21 @@ LazyListView { Behavior on y { enabled: notif.LazyListView.ready - Anim { - type: Anim.DefaultSpatial - } - } - - Behavior on opacity { Anim {} } - Behavior on scale { + Behavior on opacity { Anim { - type: Anim.DefaultSpatial + type: Anim.DefaultEffects } } + Behavior on scale { + Anim {} + } + Behavior on x { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } @@ -156,6 +152,5 @@ LazyListView { target: root.container property: "contentY" - type: Anim.DefaultSpatial } } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 2920af74..f75b1d70 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -32,9 +32,9 @@ StyledRect { } readonly property int nonAnimHeight: { - const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.small / 2) : 0); + const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0); const columnHeight = headerHeight + notifList.layoutHeight; - return Math.round(Math.max(TokenConfig.sizes.notifs.image, columnHeight) + Tokens.padding.normal * 2); + return Math.round(Math.max(TokenConfig.sizes.notifs.image, columnHeight) + Tokens.padding.medium * 2); } readonly property bool expanded: props.expandedNotifs.includes(modelData) @@ -57,13 +57,11 @@ StyledRect { implicitHeight: nonAnimHeight clip: true - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } RowLayout { @@ -72,9 +70,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop @@ -115,7 +113,7 @@ StyledRect { MaterialIcon { text: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency) color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.medium } } @@ -127,6 +125,7 @@ StyledRect { Loader { asynchronous: true anchors.centerIn: parent + anchors.verticalCenterOffset: sourceComponent === materialIconComp ? 1 : 0 sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } @@ -159,7 +158,7 @@ StyledRect { id: column Layout.fillWidth: true - spacing: root.expanded ? Math.round(Tokens.spacing.small / 2) : 0 + spacing: root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0 Behavior on spacing { Anim {} @@ -170,13 +169,13 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.small StyledText { Layout.fillWidth: true text: root.modelData color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small elide: Text.ElideRight } @@ -184,12 +183,12 @@ StyledRect { animate: true text: root.activeNotifs[0]?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledRect { - implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 - implicitHeight: groupCount.implicitHeight + Tokens.padding.small + implicitWidth: expandBtn.implicitWidth + Tokens.padding.large + implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) radius: Tokens.rounding.full @@ -203,35 +202,31 @@ StyledRect { id: expandBtn anchors.centerIn: parent - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { id: groupCount - Layout.leftMargin: Tokens.padding.small / 2 + Layout.leftMargin: Tokens.padding.extraSmall / 2 animate: true text: root.notifCount - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.small + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.small } MaterialIcon { - Layout.rightMargin: -Tokens.padding.small / 2 + Layout.rightMargin: -Tokens.padding.extraSmall / 2 text: "expand_more" - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurfaceVariant rotation: root.expanded ? 180 : 0 - Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.smaller / 2) : 0 + Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.extraSmall) : 0 Behavior on rotation { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on Layout.topMargin { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 72b8b3a8..a34fe5eb 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -22,7 +22,7 @@ LazyListView { anchors.right: parent.right implicitHeight: contentHeight - spacing: Math.round(Tokens.spacing.small / 2) + spacing: Math.round(Tokens.spacing.extraSmall) asynchronous: true readyDelay: 1 @@ -107,6 +107,7 @@ LazyListView { onFinished: notif.modelData?.unlock(notif) Anim { + type: Anim.DefaultEffects target: notif property: "opacity" to: 0 @@ -131,13 +132,13 @@ LazyListView { Behavior on y { enabled: notif.LazyListView.ready - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -145,9 +146,7 @@ LazyListView { } Behavior on x { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index 108c51a2..bb290fce 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import Caelestia import Caelestia.Config import qs.components @@ -19,9 +20,7 @@ Item { opacity: 1 - offsetScale Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { @@ -30,13 +29,14 @@ Item { anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left - anchors.margins: Tokens.padding.large + anchors.leftMargin: Tokens.padding.large + anchors.margins: CUtils.clamp(anchors.leftMargin - Config.border.thickness, 0, anchors.leftMargin) anchors.bottomMargin: 0 active: root.shouldBeActive || root.visible sourceComponent: Content { - implicitWidth: Tokens.sizes.sidebar.width - Tokens.padding.large * 2 + implicitWidth: Tokens.sizes.sidebar.width - content.anchors.leftMargin - content.anchors.margins props: root.props visibilities: root.visibilities } diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index e03c546f..eadf0f8c 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -13,6 +13,8 @@ Item { required property BarPopouts.Wrapper popouts required property matrix4x4 deformMatrix + readonly property real nonAnimHeight: idleInhibit.nonAnimHeight + record.nonAnimHeight + toggles.implicitHeight + layout.spacing * 2 + implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight @@ -20,17 +22,23 @@ Item { id: layout anchors.fill: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium - IdleInhibit {} + IdleInhibit { + id: idleInhibit + } Record { + id: record + props: root.props visibilities: root.visibilities z: 1 } Toggles { + id: toggles + visibilities: root.visibilities popouts: root.popouts } diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 13125f2c..2e462b46 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -43,7 +43,7 @@ Loader { anchors.fill: parent anchors.rightMargin: -parent.width * (1 - root.deformMatrix.m11) / 2 // Additional bit to account for deform anchors.bottomMargin: -parent.height * 0.1 // Additional bit to account for overshoot - topLeftRadius: Tokens.rounding.large + topLeftRadius: Tokens.rounding.extraLarge color: Colours.palette.m3scrim } @@ -138,15 +138,15 @@ Loader { StyledRect { anchors.centerIn: parent - radius: Tokens.rounding.large + radius: Tokens.rounding.extraLarge color: Colours.palette.m3surfaceContainerHigh scale: 0 Component.onCompleted: scale = Qt.binding(() => root.props.recordingConfirmDelete ? 1 : 0) - width: Math.min(parent.width - Tokens.padding.large * 2, implicitWidth) - implicitWidth: deleteConfirmationLayout.implicitWidth + Tokens.padding.large * 3 - implicitHeight: deleteConfirmationLayout.implicitHeight + Tokens.padding.large * 3 + width: Math.min(parent.width - Tokens.padding.extraLargeIncreased, implicitWidth) + implicitWidth: deleteConfirmationLayout.implicitWidth + Tokens.padding.extraExtraLarge + implicitHeight: deleteConfirmationLayout.implicitHeight + Tokens.padding.extraExtraLarge MouseArea { anchors.fill: parent @@ -164,25 +164,25 @@ Loader { anchors.fill: parent anchors.margins: Tokens.padding.large * 1.5 - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: qsTr("Delete recording?") - font.pointSize: Tokens.font.size.large + font: Tokens.font.body.large } StyledText { Layout.fillWidth: true text: qsTr("Recording '%1' will be permanently deleted.").arg(deleteConfirmation.path) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small wrapMode: Text.WrapAtWordBoundaryOrAnywhere } RowLayout { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium Layout.alignment: Qt.AlignRight - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium TextButton { text: qsTr("Cancel") @@ -202,14 +202,14 @@ Loader { } Behavior on scale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 958d90e6..2a9af11c 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Caelestia import Caelestia.Config import qs.components import qs.modules.sidebar as Sidebar @@ -24,12 +25,14 @@ Item { reloadableId: "utilities" } readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) + readonly property real totalPadding: content.anchors.margins + CUtils.clamp(content.anchors.margins - Config.border.thickness, 0, content.anchors.margins) + readonly property real nonAnimHeight: ((content.item as Content)?.nonAnimHeight ?? 0) + totalPadding property real offsetScale: shouldBeActive ? 0 : 1 property real sidebarLerp visible: offsetScale < 1 anchors.bottomMargin: (-implicitHeight - 5) * offsetScale - implicitHeight: content.implicitHeight + content.anchors.margins * 2 + implicitHeight: content.implicitHeight + totalPadding implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * horizontalStretch * sidebarLerp + Tokens.sizes.utilities.width * (1 - sidebarLerp) opacity: 1 - offsetScale @@ -64,9 +67,7 @@ Item { ] Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { @@ -80,7 +81,7 @@ Item { active: root.shouldBeActive || root.visible sourceComponent: Content { - implicitWidth: root.implicitWidth - content.anchors.margins * 2 + implicitWidth: root.implicitWidth - root.totalPadding props: root.props visibilities: root.visibilities popouts: root.popouts diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index c37ef3c5..c5244c93 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -8,10 +8,12 @@ import qs.services StyledRect { id: root - Layout.fillWidth: true - implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Tokens.padding.large * 2 + readonly property real nonAnimHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + Layout.fillWidth: true + implicitHeight: nonAnimHeight + + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer clip: true @@ -22,11 +24,11 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.large radius: Tokens.rounding.full color: IdleInhibitor.enabled ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer @@ -37,7 +39,7 @@ StyledRect { anchors.centerIn: parent text: "coffee" color: IdleInhibitor.enabled ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } @@ -48,7 +50,7 @@ StyledRect { StyledText { Layout.fillWidth: true text: qsTr("Keep Awake") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium elide: Text.ElideRight } @@ -56,7 +58,7 @@ StyledRect { Layout.fillWidth: true text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small elide: Text.ElideRight } } @@ -73,7 +75,7 @@ StyledRect { asynchronous: true anchors.bottom: parent.bottom anchors.left: parent.left - anchors.topMargin: Tokens.spacing.larger + anchors.topMargin: Tokens.spacing.large anchors.bottomMargin: IdleInhibitor.enabled ? Tokens.padding.large : -implicitHeight anchors.leftMargin: Tokens.padding.large @@ -83,8 +85,8 @@ StyledRect { Component.onCompleted: active = Qt.binding(() => opacity > 0) sourceComponent: StyledRect { - implicitWidth: activeText.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: activeText.implicitHeight + Tokens.padding.small * 2 + implicitWidth: activeText.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: activeText.implicitHeight + Tokens.padding.small radius: Tokens.rounding.full color: Colours.palette.m3primary @@ -95,14 +97,12 @@ StyledRect { anchors.centerIn: parent text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, GlobalConfig.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) color: Colours.palette.m3onPrimary - font.pointSize: Math.round(Tokens.font.size.small * 0.9) + font: Tokens.font.body.builders.small.size(Math.round(Tokens.font.body.small.pointSize * 0.9)).build() } } Behavior on anchors.bottomMargin { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on opacity { @@ -117,8 +117,6 @@ StyledRect { } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index 43a84076..c0d7e0c6 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -12,11 +12,12 @@ StyledRect { required property var props required property DrawerVisibilities visibilities + readonly property real nonAnimHeight: btnLayout.implicitHeight + listOrControls.implicitHeight + layout.spacing + layout.anchors.margins * 2 Layout.fillWidth: true implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -24,16 +25,17 @@ StyledRect { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium RowLayout { - spacing: Tokens.spacing.normal - z: 1 + id: btnLayout + + spacing: Tokens.spacing.medium StyledRect { implicitWidth: implicitHeight implicitHeight: { - const h = icon.implicitHeight + Tokens.padding.smaller * 2; + const h = icon.implicitHeight + Tokens.padding.large; return h - (h % 2); } @@ -48,7 +50,7 @@ StyledRect { anchors.verticalCenterOffset: 1.5 text: "screen_record" color: Recorder.running ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } @@ -59,7 +61,7 @@ StyledRect { StyledText { Layout.fillWidth: true text: qsTr("Screen Recorder") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium elide: Text.ElideRight } @@ -67,7 +69,7 @@ StyledRect { Layout.fillWidth: true text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small elide: Text.ElideRight } } @@ -187,14 +189,14 @@ StyledRect { id: recordingControls RowLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledRect { radius: Tokens.rounding.full color: Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error - implicitWidth: recText.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: recText.implicitHeight + Tokens.padding.smaller * 2 + implicitWidth: recText.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: recText.implicitHeight + Tokens.padding.large StyledText { id: recText @@ -203,7 +205,7 @@ StyledRect { animate: true text: Recorder.paused ? "PAUSED" : "REC" color: Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError - font.family: Tokens.font.family.mono + font: Tokens.font.mono.medium } Behavior on implicitWidth { @@ -246,7 +248,7 @@ StyledRect { return qsTr("Recording for %1").arg(time); } - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } Item { @@ -256,10 +258,10 @@ StyledRect { IconButton { label.animate: true icon: Recorder.paused ? "play_arrow" : "pause" - toggle: true + isToggle: true checked: Recorder.paused type: IconButton.Tonal - font.pointSize: Tokens.font.size.large + font: Tokens.font.icon.large onClicked: { Recorder.togglePause(); internalChecked = Recorder.paused; @@ -270,7 +272,7 @@ StyledRect { icon: "stop" inactiveColour: Colours.palette.m3error inactiveOnColour: Colours.palette.m3onError - font.pointSize: Tokens.font.size.large + font: Tokens.font.icon.large onClicked: Recorder.stop() } } diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index 951f7f09..ab727817 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -27,19 +27,19 @@ ColumnLayout { onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded RowLayout { - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium MaterialIcon { Layout.alignment: Qt.AlignVCenter text: "list" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } StyledText { Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true text: qsTr("Recordings") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } IconButton { @@ -62,7 +62,7 @@ ColumnLayout { Layout.fillWidth: true Layout.rightMargin: -Tokens.spacing.small - implicitHeight: (Tokens.font.size.larger + Tokens.padding.small) * (root.props.recordingListExpanded ? 10 : 3) + implicitHeight: (Tokens.font.body.large.pointSize + Tokens.padding.small) * (root.props.recordingListExpanded ? 10 : 3) clip: true StyledScrollBar.vertical: StyledScrollBar { @@ -78,13 +78,13 @@ ColumnLayout { anchors.left: list.contentItem.left anchors.right: list.contentItem.right anchors.rightMargin: Tokens.spacing.small - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall Component.onCompleted: baseName = modelData.baseName StyledText { Layout.fillWidth: true - Layout.rightMargin: Tokens.spacing.small / 2 + Layout.rightMargin: Tokens.spacing.extraSmall text: { const time = recording.baseName; const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/); @@ -129,31 +129,25 @@ ColumnLayout { add: Transition { Anim { + type: Anim.DefaultEffects property: "opacity" from: 0 to: 1 } - Anim { - property: "scale" - from: 0.5 - to: 1 - } } remove: Transition { Anim { + type: Anim.DefaultEffects property: "opacity" to: 0 } - Anim { - property: "scale" - to: 0.5 - } } displaced: Transition { Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" to: 1 } Anim { @@ -175,14 +169,16 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter text: "scan_delete" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge opacity: root.props.recordingListExpanded ? 1 : 0 scale: root.props.recordingListExpanded ? 1 : 0 Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -195,7 +191,7 @@ ColumnLayout { } RowLayout { - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium MaterialIcon { Layout.alignment: Qt.AlignHCenter @@ -207,7 +203,9 @@ ColumnLayout { Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -227,14 +225,14 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 8c294eb3..d145700a 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -3,10 +3,12 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell.Bluetooth +import Caelestia.Components import Caelestia.Config import qs.components import qs.components.controls import qs.services +import qs.modules.nexus import qs.modules.bar.popouts as BarPopouts StyledRect { @@ -38,9 +40,9 @@ StyledRect { readonly property bool needExtraRow: quickToggles.length > 6 Layout.fillWidth: true - implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -48,31 +50,31 @@ StyledRect { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: qsTr("Quick Toggles") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } QuickToggleRow { - rowModel: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles + model: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles } QuickToggleRow { visible: root.needExtraRow - rowModel: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] + model: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] } } - component QuickToggleRow: RowLayout { - property var rowModel: [] + component QuickToggleRow: ButtonRow { + property alias model: repeater.model Layout.fillWidth: true spacing: Tokens.spacing.small Repeater { - model: parent.rowModel + id: repeater delegate: DelegateChooser { role: "id" @@ -114,10 +116,10 @@ StyledRect { delegate: Toggle { icon: "settings" inactiveOnColour: Colours.palette.m3onSurfaceVariant - toggle: false + isToggle: false onClicked: { root.visibilities.utilities = false; - root.popouts.detach("network"); + WindowFactory.create(); } } } @@ -143,7 +145,7 @@ StyledRect { icon: "vpn_key" checked: VPN.connected && VPN.status.state !== "needs-auth" && VPN.status.state !== "error" enabled: !VPN.connecting - toggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error" + isToggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error" inactiveOnColour: Colours.palette.m3onSurfaceVariant onClicked: VPN.toggle() } @@ -153,18 +155,10 @@ StyledRect { } component Toggle: IconButton { - Layout.fillWidth: true - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) - radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) - toggle: true - radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial - radiusAnim.easing: Tokens.anim.expressiveFastSpatial - - Behavior on Layout.preferredWidth { - Anim { - type: Anim.FastSpatial - } - } + fillWidth: true + isToggle: true + isRound: true + shapeMorph: true } } diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index 5247e773..93d68937 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -13,9 +13,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right - implicitHeight: layout.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.large - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3successContainer; @@ -50,13 +50,13 @@ StyledRect { id: layout anchors.fill: parent - anchors.margins: Tokens.padding.smaller - anchors.leftMargin: Tokens.padding.normal - anchors.rightMargin: Tokens.padding.normal - spacing: Tokens.spacing.normal + anchors.margins: Tokens.padding.small + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + spacing: Tokens.spacing.medium StyledRect { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3success; @@ -68,7 +68,7 @@ StyledRect { } implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.large MaterialIcon { id: icon @@ -84,7 +84,7 @@ StyledRect { return Colours.palette.m3onError; return Colours.palette.m3onSurfaceVariant; } - font.pointSize: Math.round(Tokens.font.size.large * 1.2) + fontStyle: Tokens.font.icon.builders.large.scale(1.2).build() } } @@ -106,7 +106,7 @@ StyledRect { return Colours.palette.m3onErrorContainer; return Colours.palette.m3onSurface; } - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small elide: Text.ElideRight } diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index 21f2934e..2828d6f6 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -23,7 +23,7 @@ Item { return false; } - implicitWidth: Tokens.sizes.utilities.toastWidth - Tokens.padding.normal * 2 + implicitWidth: Tokens.sizes.utilities.toastWidth - Tokens.padding.medium * 2 implicitHeight: { let h = -spacing; for (let i = 0; i < repeater.count; i++) { @@ -111,7 +111,6 @@ Item { properties: "opacity,scale" from: 0 to: 1 - type: Anim.DefaultSpatial } ParallelAnimation { @@ -120,6 +119,7 @@ Item { onFinished: toast.modelData.unlock(toast) Anim { + type: Anim.DefaultEffects target: toast property: "opacity" to: 0 @@ -138,7 +138,9 @@ Item { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -146,9 +148,7 @@ Item { } Behavior on anchors.bottomMargin { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index c4fbbd53..a349cbb5 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -21,7 +21,7 @@ ColumnLayout { Layout.leftMargin: Tokens.padding.large Layout.rightMargin: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -31,10 +31,10 @@ ColumnLayout { StyledRect { color: Colours.palette.m3primary - radius: Tokens.rounding.small + radius: Tokens.rounding.medium - implicitWidth: moveToWsIcon.implicitWidth + Tokens.padding.small * 2 - implicitHeight: moveToWsIcon.implicitHeight + Tokens.padding.small + implicitWidth: moveToWsIcon.implicitWidth + Tokens.padding.small + implicitHeight: moveToWsIcon.implicitHeight + Tokens.padding.extraSmall StateLayer { color: Colours.palette.m3onPrimary @@ -49,27 +49,27 @@ ColumnLayout { animate: true text: root.moveToWsExpanded ? "expand_more" : "keyboard_arrow_right" color: Colours.palette.m3onPrimary - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } WrapperItem { Layout.fillWidth: true - Layout.leftMargin: Tokens.padding.large * 2 - Layout.rightMargin: Tokens.padding.large * 2 + Layout.leftMargin: Tokens.padding.extraLargeIncreased + Layout.rightMargin: Tokens.padding.extraLargeIncreased Layout.preferredHeight: root.moveToWsExpanded ? implicitHeight : 0 clip: true - topMargin: Tokens.spacing.normal - bottomMargin: Tokens.spacing.normal + topMargin: Tokens.spacing.medium + bottomMargin: Tokens.spacing.medium GridLayout { id: wsGrid - rowSpacing: Tokens.spacing.smaller - columnSpacing: Tokens.spacing.normal + rowSpacing: Tokens.spacing.medium + columnSpacing: Tokens.spacing.medium columns: 5 Repeater { @@ -103,7 +103,7 @@ ColumnLayout { Layout.rightMargin: Tokens.padding.large Layout.bottomMargin: Tokens.padding.large - spacing: root.client?.lastIpcObject.floating ? Tokens.spacing.normal : Tokens.spacing.small + spacing: root.client?.lastIpcObject.floating ? Tokens.spacing.medium : Tokens.spacing.small Button { color: Colours.palette.m3secondaryContainer @@ -142,10 +142,10 @@ ColumnLayout { signal clicked - radius: Tokens.rounding.small + radius: Tokens.rounding.medium Layout.fillWidth: true - implicitHeight: label.implicitHeight + Tokens.padding.small * 2 + implicitHeight: label.implicitHeight + Tokens.padding.small StateLayer { id: stateLayer @@ -161,7 +161,7 @@ ColumnLayout { animate: true color: parent.onColor - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } } } diff --git a/modules/windowinfo/Details.qml b/modules/windowinfo/Details.qml index 1820409f..50babbda 100644 --- a/modules/windowinfo/Details.qml +++ b/modules/windowinfo/Details.qml @@ -14,29 +14,28 @@ ColumnLayout { spacing: Tokens.spacing.small Label { - Layout.topMargin: Tokens.padding.large * 2 + Layout.topMargin: Tokens.padding.extraLargeIncreased text: root.client?.title ?? qsTr("No active client") wrapMode: Text.WrapAtWordBoundaryOrAnywhere - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } Label { text: root.client?.lastIpcObject.class ?? qsTr("No active client") color: Colours.palette.m3tertiary - font.pointSize: Tokens.font.size.larger + font: Tokens.font.body.large } StyledRect { Layout.fillWidth: true Layout.preferredHeight: 1 - Layout.leftMargin: Tokens.padding.large * 2 - Layout.rightMargin: Tokens.padding.large * 2 - Layout.topMargin: Tokens.spacing.normal - Layout.bottomMargin: Tokens.spacing.large + Layout.leftMargin: Tokens.padding.extraLargeIncreased + Layout.rightMargin: Tokens.padding.extraLargeIncreased + Layout.topMargin: Tokens.spacing.medium + Layout.bottomMargin: Tokens.spacing.largeIncreased color: Colours.palette.m3secondary } @@ -134,7 +133,7 @@ ColumnLayout { Layout.rightMargin: Tokens.padding.large Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium MaterialIcon { id: icon @@ -149,7 +148,7 @@ ColumnLayout { text: detail.text elide: Text.ElideRight - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } } diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml index fed85891..4ff03480 100644 --- a/modules/windowinfo/Preview.qml +++ b/modules/windowinfo/Preview.qml @@ -15,7 +15,7 @@ Item { required property ShellScreen screen required property HyprlandToplevel client - Layout.preferredWidth: preview.implicitWidth + Tokens.padding.large * 2 + Layout.preferredWidth: preview.implicitWidth + Tokens.padding.extraLargeIncreased Layout.fillHeight: true StyledClippingRect { @@ -25,12 +25,12 @@ Item { anchors.top: parent.top anchors.bottom: label.top anchors.topMargin: Tokens.padding.large - anchors.bottomMargin: Tokens.spacing.normal + anchors.bottomMargin: Tokens.spacing.medium implicitWidth: view.implicitWidth color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.small + radius: Tokens.rounding.medium Loader { asynchronous: true @@ -44,22 +44,21 @@ Item { Layout.alignment: Qt.AlignHCenter text: "web_asset_off" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.extraLarge * 3 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).build() } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("No active client") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 + font: Tokens.font.body.builders.large.size(28).weight(Font.Medium).build() } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Try switching to a window") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.large + font: Tokens.font.body.large } } } diff --git a/modules/windowinfo/WindowInfo.qml b/modules/windowinfo/WindowInfo.qml index 7ee35c29..212cb717 100644 --- a/modules/windowinfo/WindowInfo.qml +++ b/modules/windowinfo/WindowInfo.qml @@ -21,7 +21,7 @@ Item { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Preview { screen: root.screen @@ -29,7 +29,7 @@ Item { } ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Layout.preferredWidth: Tokens.sizes.winfo.detailsWidth Layout.fillHeight: true @@ -39,7 +39,7 @@ Item { Layout.fillHeight: true color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.normal + radius: Tokens.rounding.large Details { client: root.client @@ -51,7 +51,7 @@ Item { Layout.preferredHeight: buttons.implicitHeight color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.normal + radius: Tokens.rounding.large Buttons { id: buttons diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index 062959ce..76e07c45 100644 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -1 +1,18 @@ +find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick QuickControls2 Concurrent Sql Network DBus) +find_package(PkgConfig REQUIRED) +pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) +pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) +pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) +pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET) +if(NOT Cava_FOUND) + pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) +endif() +include(cmake/sensorslib.cmake) + +set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") +qt_standard_project_setup(REQUIRES 6.9) + +include(cmake/pch.cmake) +include(cmake/qml-module.cmake) + add_subdirectory(src/Caelestia) diff --git a/plugin/cmake/pch.cmake b/plugin/cmake/pch.cmake new file mode 100644 index 00000000..6027de6f --- /dev/null +++ b/plugin/cmake/pch.cmake @@ -0,0 +1,14 @@ +add_library(caelestia-pch INTERFACE) +target_precompile_headers(caelestia-pch INTERFACE + + + + + + + + + + + +) diff --git a/plugin/cmake/qml-module.cmake b/plugin/cmake/qml-module.cmake new file mode 100644 index 00000000..29dc5fc2 --- /dev/null +++ b/plugin/cmake/qml-module.cmake @@ -0,0 +1,52 @@ +message(STATUS "QML install dir: ${CMAKE_INSTALL_PREFIX}/${INSTALL_QMLDIR}") + +function(qml_module arg_TARGET) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;QML_FILES;QML_SINGLETONS;DEPENDENCIES;IMPORTS;OPTIONAL_IMPORTS;DEFAULT_IMPORTS;LIBRARIES") + + set_source_files_properties(${arg_QML_SINGLETONS} PROPERTIES QT_QML_SINGLETON_TYPE TRUE) + + qt_add_qml_module(${arg_TARGET} + URI ${arg_URI} + SOURCES ${arg_SOURCES} + QML_FILES ${arg_QML_FILES} ${arg_QML_SINGLETONS} + DEPENDENCIES ${arg_DEPENDENCIES} + IMPORTS ${arg_IMPORTS} + OPTIONAL_IMPORTS ${arg_OPTIONAL_IMPORTS} + DEFAULT_IMPORTS ${arg_DEFAULT_IMPORTS} + ) + + qt_query_qml_module(${arg_TARGET} + URI module_uri + PLUGIN_TARGET module_plugin_target + TARGET_PATH module_target_path + QMLDIR module_qmldir + TYPEINFO module_typeinfo + ) + + message(STATUS "Created QML module: ${module_uri}") + + # Modules can't link together properly if the backing target and plugin target + # are in the same dir (because diff modules can't access each other). + # So install backing targets to QMLDIR/.../lib/ instead. + string(REPLACE "/" ";" uri_parts "${module_target_path}") + list(GET uri_parts 0 top_level) + set(backing_lib_dir "${INSTALL_QMLDIR}/${top_level}/lib") + set(module_dir "${INSTALL_QMLDIR}/${module_target_path}") + + install(TARGETS ${arg_TARGET} + LIBRARY DESTINATION "${backing_lib_dir}" + RUNTIME DESTINATION "${backing_lib_dir}" + ) + install(TARGETS "${module_plugin_target}" + LIBRARY DESTINATION "${module_dir}" + RUNTIME DESTINATION "${module_dir}" + ) + install(FILES "${module_qmldir}" DESTINATION "${module_dir}") + install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") + + target_link_libraries(${arg_TARGET} PRIVATE caelestia-pch Qt::Core Qt::Qml ${arg_LIBRARIES}) + + # Add backing target dir to plugin rpath so it can find its backing target + file(RELATIVE_PATH plugin_to_lib "/${module_target_path}" "/${top_level}/lib") + set_property(TARGET ${module_plugin_target} APPEND PROPERTY INSTALL_RPATH "$ORIGIN/${plugin_to_lib}") +endfunction() diff --git a/plugin/cmake/sensorslib.cmake b/plugin/cmake/sensorslib.cmake new file mode 100644 index 00000000..67d9117a --- /dev/null +++ b/plugin/cmake/sensorslib.cmake @@ -0,0 +1,9 @@ +find_library(SENSORS_LIBRARY NAMES sensors REQUIRED) +find_path(SENSORS_INCLUDE_DIR NAMES sensors/sensors.h REQUIRED) +if(NOT TARGET Sensors::Sensors) + add_library(Sensors::Sensors UNKNOWN IMPORTED) + set_target_properties(Sensors::Sensors PROPERTIES + IMPORTED_LOCATION "${SENSORS_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${SENSORS_INCLUDE_DIR}" + ) +endif() diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp index a4703c86..e1ff8b23 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.cpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp @@ -28,6 +28,14 @@ void BlobGroup::setColor(const QColor& c) { markDirty(); } +void BlobGroup::setCornerFill(bool e) { + if (m_cornerFill == e) + return; + m_cornerFill = e; + emit cornerFillChanged(); + markDirty(); +} + void BlobGroup::addShape(BlobShape* shape) { if (!shape || m_shapes.contains(shape)) return; diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp index e09125a9..a73ffc99 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.hpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp @@ -13,6 +13,7 @@ class BlobGroup : public QObject { QML_ELEMENT Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged) Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + Q_PROPERTY(bool cornerFill READ cornerFill WRITE setCornerFill NOTIFY cornerFillChanged) public: explicit BlobGroup(QObject* parent = nullptr); @@ -26,6 +27,10 @@ public: void setColor(const QColor& c); + bool cornerFill() const { return m_cornerFill; } + + void setCornerFill(bool e); + void addShape(BlobShape* shape); void removeShape(BlobShape* shape); @@ -43,10 +48,12 @@ public: signals: void smoothingChanged(); void colorChanged(); + void cornerFillChanged(); private: qreal m_smoothing = 32.0; QColor m_color{ 0x44, 0x88, 0xff }; + bool m_cornerFill = true; QList m_shapes; BlobInvertedRect* m_invertedRect = nullptr; bool m_physicsUpdated = false; diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp index 15486d83..f78d346d 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -173,11 +173,24 @@ bool BlobRect::isExcluded(const BlobShape* other) const { return false; } +bool BlobRect::isCornerExcluded(const BlobShape* other) const { + for (const auto& ptr : m_excludeCorners) { + if (ptr == other) + return true; + } + return false; +} + QQmlListProperty BlobRect::exclude() { return QQmlListProperty( this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast); } +QQmlListProperty BlobRect::excludeCorners() { + return QQmlListProperty(this, nullptr, &excludeCornersAppend, &excludeCornersCount, &excludeCornersAt, + &excludeCornersClear, &excludeCornersReplace, &excludeCornersRemoveLast); +} + void BlobRect::excludeAppend(QQmlListProperty* prop, BlobRect* rect) { auto* self = static_cast(prop->object); self->m_exclude.append(rect); @@ -224,6 +237,52 @@ void BlobRect::excludeRemoveLast(QQmlListProperty* prop) { emit self->excludeChanged(); } +void BlobRect::excludeCornersAppend(QQmlListProperty* prop, BlobRect* rect) { + auto* self = static_cast(prop->object); + self->m_excludeCorners.append(rect); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + +qsizetype BlobRect::excludeCornersCount(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + return self->m_excludeCorners.size(); +} + +BlobRect* BlobRect::excludeCornersAt(QQmlListProperty* prop, qsizetype index) { + auto* self = static_cast(prop->object); + return self->m_excludeCorners.at(index); +} + +void BlobRect::excludeCornersClear(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_excludeCorners.isEmpty()) + return; + self->m_excludeCorners.clear(); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + +void BlobRect::excludeCornersReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect) { + auto* self = static_cast(prop->object); + self->m_excludeCorners[index] = rect; + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + +void BlobRect::excludeCornersRemoveLast(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_excludeCorners.isEmpty()) + return; + self->m_excludeCorners.removeLast(); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + void BlobRect::checkAtRest(float speed) { constexpr float kEpsilon = 0.002f; const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && diff --git a/plugin/src/Caelestia/Blobs/blobrect.hpp b/plugin/src/Caelestia/Blobs/blobrect.hpp index d2d6ad45..354cc265 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.hpp +++ b/plugin/src/Caelestia/Blobs/blobrect.hpp @@ -14,6 +14,7 @@ class BlobRect : public BlobShape { Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged) Q_PROPERTY(QQmlListProperty exclude READ exclude NOTIFY excludeChanged) + Q_PROPERTY(QQmlListProperty excludeCorners READ excludeCorners NOTIFY excludeCornersChanged) Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged) Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged) Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged) @@ -52,8 +53,10 @@ public: } QQmlListProperty exclude(); + QQmlListProperty excludeCorners(); bool isExcluded(const BlobShape* other) const override; + bool isCornerExcluded(const BlobShape* other) const override; void cornerRadii(float out[4]) const override; qreal topLeftRadius() const { return m_topLeftRadius; } @@ -77,6 +80,7 @@ signals: void dampingChanged(); void deformScaleChanged(); void excludeChanged(); + void excludeCornersChanged(); void topLeftRadiusChanged(); void topRightRadiusChanged(); void bottomLeftRadiusChanged(); @@ -116,6 +120,7 @@ private: qreal m_bottomRightRadius = -1; QList> m_exclude; + QList> m_excludeCorners; static void excludeAppend(QQmlListProperty* prop, BlobRect* rect); static qsizetype excludeCount(QQmlListProperty* prop); @@ -123,4 +128,11 @@ private: static void excludeClear(QQmlListProperty* prop); static void excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect); static void excludeRemoveLast(QQmlListProperty* prop); + + static void excludeCornersAppend(QQmlListProperty* prop, BlobRect* rect); + static qsizetype excludeCornersCount(QQmlListProperty* prop); + static BlobRect* excludeCornersAt(QQmlListProperty* prop, qsizetype index); + static void excludeCornersClear(QQmlListProperty* prop); + static void excludeCornersReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect); + static void excludeCornersRemoveLast(QQmlListProperty* prop); }; diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index cfa11c44..61181819 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -32,6 +32,18 @@ static float cpuSmoothstep(float edge0, float edge1, float x) { return t * t * (3.0f - 2.0f * t); } +static float cornerFillFactor(float sd, float smoothFactor) { + // Continuous two-sided window. The corner is squared (factor -> 0) only within + // ±smoothFactor of the neighbour's edge (the visible junction); it keeps its full + // radius both far outside the neighbour and deep inside it (where it is buried and + // squaring would only crease the interior). C0-continuous across sd = 0 — unlike the + // old `if (sd >= 0)` branch, which snapped the radius full<->square (factor 1<->0) on + // sub-pixel motion as a corner crossed the edge, flickering the fill bridge in/out. + const float outside = cpuSmoothstep(0.0f, smoothFactor, sd); // 0 at edge, ->1 far outside + const float inside = cpuSmoothstep(0.0f, -smoothFactor, sd); // 0 at edge, ->1 deep inside + return std::max(outside, inside); +} + BlobShape::BlobShape(QQuickItem* parent) : QQuickItem(parent) { setFlag(ItemHasContents); @@ -288,10 +300,12 @@ void BlobShape::updatePolish() { // Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU) const float smoothFactor = pad; constexpr float minR = 2.0f; + const bool cornerFill = m_group->cornerFill(); const auto rectCount = m_cachedRects.size(); for (qsizetype i = 0; i < rectCount; ++i) { auto& ri = m_cachedRects[i]; const int riExcludeMask = ri.excludeMask; + BlobShape* const si = rectShapes[i]; float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; @@ -299,19 +313,28 @@ void BlobShape::updatePolish() { const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; - for (qsizetype j = 0; j < rectCount; ++j) { + for (qsizetype j = 0; cornerFill && j < rectCount; ++j) { if (j == i) continue; if (riExcludeMask & (1 << j)) continue; + BlobShape* const sj = rectShapes[j]; + if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) + continue; const auto& rj = m_cachedRects[j]; - fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); - fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); - fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); - fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); + // Square each corner only near rj's edge; keep full radius far outside AND + // deep inside rj (buried, so it can't crease the visible junction). + const float sdTr = cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh); + const float sdBr = cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh); + const float sdBl = cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh); + const float sdTl = cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh); + fTr = std::min(fTr, cornerFillFactor(sdTr, smoothFactor)); + fBr = std::min(fBr, cornerFillFactor(sdBr, smoothFactor)); + fBl = std::min(fBl, cornerFillFactor(sdBl, smoothFactor)); + fTl = std::min(fTl, cornerFillFactor(sdTl, smoothFactor)); } - if (m_cachedHasInverted) { + if (cornerFill && m_cachedHasInverted) { const float icx = m_cachedInvertedInner[0]; const float icy = m_cachedInvertedInner[1]; const float ihw = m_cachedInvertedInner[2]; diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp index c05a40d8..e179531a 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.hpp +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -49,6 +49,8 @@ protected: virtual bool isExcluded(const BlobShape* /*other*/) const { return false; } + virtual bool isCornerExcluded(const BlobShape* /*other*/) const { return false; } + virtual void cornerRadii(float out[4]) const; virtual void updatePhysics() {} diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index c002fd25..138928fa 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -41,22 +41,26 @@ float sdBox(vec2 p, vec2 center, vec2 halfSize) { } float smin(float a, float b, float k) { - // Cubic smooth min (C2 continuous — no curvature kinks at blend boundary) - float h = max(k - abs(a - b), 0.0) / k; - return min(a, b) - h * h * h * k * (1.0/6.0); + // Circular smooth min — the blend fillet is a true circular arc of radius k, + // tangent to both surfaces (not a polynomial/squircle curve). Deviates from + // min(a, b) only in the corner region where BOTH a < k and b < k (unlike the + // cubic it replaced, which deviated over the whole band |a - b| < k). Always + // <= min(a, b); max blend depth at a == b is (sqrt(2) - 1) * k. It is C1 but + // not C2 at the support boundary, so a circular-arc fillet shows the usual + // line-meets-arc curvature step — by design, that is the "circular" look. + return max(k, min(a, b)) - length(max(vec2(k) - vec2(a, b), vec2(0.0))); } float smax(float a, float b, float k) { - float h = max(k - abs(a - b), 0.0) / k; - return max(a, b) + h * h * h * k * (1.0/6.0); + // Circular smooth max — dual of smin: -smin(-a, -b, k). Always >= max(a, b). + return min(-k, max(a, b)) + length(max(vec2(a, b) + vec2(k), vec2(0.0))); } float smaxSharpA(float a, float b, float k) { - // smax variant that keeps a's boundary sharp (no inward rounding at a = 0). - // Used for the frame outer edge so it always fills to the edges. - float h = max(k - abs(a - b), 0.0) / k; - float blend = h * h * h * k * (1.0/6.0); - blend *= smoothstep(0.0, k * 0.5, -a); + // Circular smax variant that keeps a's boundary sharp (no inward rounding at + // a = 0). Used for the frame outer edge so it always fills to the edges. + float sm = min(-k, max(a, b)) + length(max(vec2(a, b) + vec2(k), vec2(0.0))); + float blend = (sm - max(a, b)) * smoothstep(0.0, k * 0.5, -a); return max(a, b) + blend; } @@ -163,8 +167,8 @@ void main() { continue; if ((excludeMask & (1 << j)) != 0) continue; - // smin only deviates from min within smoothFactor - if (abs(dArr[i] - dArr[j]) >= smoothFactor) + // Circular smin deviates from min only where BOTH dArr are < smoothFactor. + if (max(dArr[i], dArr[j]) >= smoothFactor) continue; mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor)); } @@ -193,8 +197,13 @@ void main() { // Screen-space center (with offset) and pre-computed AABB half-extents vec2 ctr = rect.xy + sinkProps.yz; - // Delay sink to absorb smin blend depth (cubic smin max = k/6) - float preOff = smoothFactor * (1.0/6.0); + // Sink onset / residual overlap: how far a rect must penetrate the border before + // the inner wall recedes to form its pocket. Too low and the wall recedes faster + // than the junction can stay convex, denting the inner edge inward near the rect's + // (squared) corners; too high and the rect nestles too deep before the wall yields. + // Tuned between the old cubic blend depth (k/6, too shallow) and the circular blend + // depth ((sqrt2-1)k): half the circular smin gap-closing distance, (2-sqrt2)k/2. + float preOff = smoothFactor * (2.0 - sqrt(2.0)) * 0.5; // Top border: track rect's BOTTOM edge, only within border thickness float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop); @@ -230,7 +239,16 @@ void main() { dInner -= sinkValue; - float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor); + // The circular smax fillet has radius kFrame; when it exceeds the border thickness + // it can't complete inside the border, so the sharp outer-box term bleeds onto the + // inner edge and bulges the inner corners (worst when thickness < smoothFactor — the + // default border is thinner than the blend radius). Clamp kFrame to the thinnest side + // so the inner edge stays a clean constant-radius arc. Each inner corner is bounded by + // its thinner adjacent side, so the global min is correct for every corner. + float minThick = min(min(innerTop - outerTop, outerBot - innerBot), + min(innerLeft - outerLeft, outerRight - innerRight)); + float kFrame = clamp(min(smoothFactor, minThick - 1.0), 1.0, smoothFactor); + float dFrame = smaxSharpA(dOuter, -dInner, kFrame); mergedSdf = smin(mergedSdf, dFrame, smoothFactor); if (dFrame < minDist) { diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 6e73f4fe..ea2ee748 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -1,61 +1,3 @@ -find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick QuickControls2 Concurrent Sql Network DBus) -find_package(PkgConfig REQUIRED) -pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) -pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) -pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) -pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET) -if(NOT Cava_FOUND) - pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) -endif() - -set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") -qt_standard_project_setup(REQUIRES 6.9) - -function(qml_module arg_TARGET) - cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;QML_FILES;LIBRARIES") - - qt_add_qml_module(${arg_TARGET} - URI ${arg_URI} - VERSION ${VERSION} - SOURCES ${arg_SOURCES} - QML_FILES ${arg_QML_FILES} - ) - - qt_query_qml_module(${arg_TARGET} - URI module_uri - VERSION module_version - PLUGIN_TARGET module_plugin_target - TARGET_PATH module_target_path - QMLDIR module_qmldir - TYPEINFO module_typeinfo - ) - - message(STATUS "Created QML module ${module_uri}, version ${module_version}") - - set(module_dir "${INSTALL_QMLDIR}/${module_target_path}") - install(TARGETS ${arg_TARGET} LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}") - install(TARGETS "${module_plugin_target}" LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}") - install(FILES "${module_qmldir}" DESTINATION "${module_dir}") - install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") - - target_link_libraries(${arg_TARGET} PRIVATE caelestia-pch Qt::Core Qt::Qml ${arg_LIBRARIES}) -endfunction() - -add_library(caelestia-pch INTERFACE) -target_precompile_headers(caelestia-pch INTERFACE - - - - - - - - - - - -) - qml_module(caelestia URI Caelestia SOURCES @@ -73,6 +15,9 @@ qml_module(caelestia PkgConfig::Qalculate ) +# Bake the shell version in so CUtils can expose it without spawning a process +target_compile_definitions(caelestia PRIVATE CAELESTIA_VERSION="${VERSION}") + add_subdirectory(Components) add_subdirectory(Config) add_subdirectory(Internal) diff --git a/plugin/src/Caelestia/Components/CMakeLists.txt b/plugin/src/Caelestia/Components/CMakeLists.txt index f880d318..ac8ce166 100644 --- a/plugin/src/Caelestia/Components/CMakeLists.txt +++ b/plugin/src/Caelestia/Components/CMakeLists.txt @@ -1,7 +1,9 @@ qml_module(caelestia-components URI Caelestia.Components SOURCES - lazylistview.hpp lazylistview.cpp + lazylistview.cpp + wavyline.cpp + buttonrow.cpp LIBRARIES Qt::Quick ) diff --git a/plugin/src/Caelestia/Components/buttonrow.cpp b/plugin/src/Caelestia/Components/buttonrow.cpp new file mode 100644 index 00000000..6177ae4f --- /dev/null +++ b/plugin/src/Caelestia/Components/buttonrow.cpp @@ -0,0 +1,132 @@ +#include "buttonrow.hpp" + +namespace caelestia::components { + +ButtonRow::ButtonRow(QQuickItem* parent) + : QQuickItem(parent) + , m_dirty(false) + , m_spacing(0.0) { + setFlag(QQuickItem::ItemHasContents, true); + QObject::connect(this, &ButtonRow::widthChanged, this, &ButtonRow::invalidate); +} + +qreal ButtonRow::spacing() const { + return m_spacing; +} + +void ButtonRow::setSpacing(qreal spacing) { + if (qFuzzyCompare(m_spacing + 1.0, spacing + 1.0)) + return; + + m_spacing = spacing; + emit spacingChanged(); + + invalidate(); +} + +void ButtonRow::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& data) { + if (change == QQuickItem::ItemChildAddedChange) { + auto* const child = data.item; + QObject::connect(child, &QQuickItem::implicitWidthChanged, this, &ButtonRow::invalidate); + QObject::connect(child, &QQuickItem::implicitHeightChanged, this, &ButtonRow::invalidate); + QObject::connect(child, &QQuickItem::visibleChanged, this, &ButtonRow::invalidate); + + const auto* childMeta = child->metaObject(); + const auto morphSignalIdx = childMeta->indexOfSignal("shapeMorphExpansionChanged()"); + if (morphSignalIdx != -1) + QObject::connect(child, childMeta->method(morphSignalIdx), this, + metaObject()->method(metaObject()->indexOfSlot("invalidate()"))); + + invalidate(); + } else if (change == QQuickItem::ItemChildRemovedChange) { + QObject::disconnect(data.item, nullptr, this, nullptr); + invalidate(); + } + + QQuickItem::itemChange(change, data); +} + +void ButtonRow::updatePolish() { + if (m_dirty) { + relayout(); + m_dirty = false; + } +} + +void ButtonRow::invalidate() { + m_dirty = true; + polish(); +} + +void ButtonRow::relayout() { + const auto allChildren = childItems(); + + QList validChildren; + for (auto* const child : allChildren) { + if (child->isVisible() && !child->inherits("QQuickRepeater")) + validChildren.append(child); + } + const auto nChildren = validChildren.size(); + const auto totalSpacing = static_cast(nChildren - 1) * m_spacing; + + qreal reservedWidth = 0; + qreal unreservedWidth = 0; + int fillWidthCount = 0; + qreal maxHeight = 0; + for (auto* const child : validChildren) { + maxHeight = qMax(maxHeight, child->implicitHeight()); + + const auto prop = child->property("fillWidth"); + if (!prop.isValid()) + continue; + + if (prop.toBool()) { + fillWidthCount++; + unreservedWidth += child->implicitWidth(); + } else { + reservedWidth += child->implicitWidth(); + } + } + + if (fillWidthCount == 0) + fillWidthCount = 1; // Avoid divide by 0 + + const auto widthPerItem = (width() - totalSpacing - reservedWidth) / static_cast(fillWidthCount); + + QList baseWidths; + baseWidths.reserve(nChildren); + for (auto* const child : validChildren) + baseWidths.append(child->property("fillWidth").toBool() ? widthPerItem : child->implicitWidth()); + + qreal accX = 0; + for (int i = 0; i < nChildren; ++i) { + auto* const child = validChildren[i]; + + // clang-format off + auto prevExtraWidth = i > 0 ? getMorphExpansion(validChildren[i - 1]) : 0.0; + auto nextExtraWidth = i < nChildren - 1 ? getMorphExpansion(validChildren[i + 1]) : 0.0; + // clang-format on + + // Items at edges push by full amount, items in middle push by half + if (i > 1) + prevExtraWidth /= 2; + if (i < nChildren - 2) + nextExtraWidth /= 2; + + child->setWidth(baseWidths[i] + getMorphExpansion(child) - prevExtraWidth - nextExtraWidth); + child->setHeight(maxHeight); + + child->setX(accX); + child->setY(0); + accX += child->width() + m_spacing; + } + + setImplicitWidth(reservedWidth + unreservedWidth + totalSpacing); + setImplicitHeight(maxHeight); +} + +qreal ButtonRow::getMorphExpansion(const QQuickItem* item) { + return item->property("shapeMorphExpansion").toReal(); +} + +} // namespace caelestia::components diff --git a/plugin/src/Caelestia/Components/buttonrow.hpp b/plugin/src/Caelestia/Components/buttonrow.hpp new file mode 100644 index 00000000..1d138d8b --- /dev/null +++ b/plugin/src/Caelestia/Components/buttonrow.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace caelestia::components { + +class ButtonRow : public QQuickItem { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + +public: + explicit ButtonRow(QQuickItem* parent = nullptr); + + qreal spacing() const; + void setSpacing(qreal spacing); + +signals: + void spacingChanged(); + +protected: + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& data) override; + void updatePolish() override; + +private slots: + void invalidate(); + +private: + void relayout(); + static qreal getMorphExpansion(const QQuickItem* item); + + bool m_dirty; + qreal m_spacing; +}; + +} // namespace caelestia::components diff --git a/plugin/src/Caelestia/Components/wavyline.cpp b/plugin/src/Caelestia/Components/wavyline.cpp new file mode 100644 index 00000000..e61b896f --- /dev/null +++ b/plugin/src/Caelestia/Components/wavyline.cpp @@ -0,0 +1,255 @@ +#include "wavyline.hpp" + +#include +#include + +namespace caelestia::controls { + +WavyLine::WavyLine(QQuickItem* parent) + : QQuickPaintedItem(parent) + , m_lineWidth(4) + , m_amplitudeMultiplier(0.5) + , m_frequency(6) + , m_startX(0) + , m_fullLength(0) + , m_color(Qt::white) + , m_waveProgress(0) + , m_pathType(Linear) + , m_startAngle(0) + , m_fullAngle(360) + , m_radius(-1) + , m_value(1) + , m_startAngleRad(0) + , m_fullAngleRad(2 * M_PI) { + setAntialiasing(true); +} + +int WavyLine::lineWidth() const { + return m_lineWidth; +} + +void WavyLine::setLineWidth(int lineWidth) { + if (m_lineWidth != lineWidth) { + m_lineWidth = lineWidth; + emit lineWidthChanged(); + update(); + } +} + +qreal WavyLine::amplitudeMultiplier() const { + return m_amplitudeMultiplier; +} + +void WavyLine::setAmplitudeMultiplier(qreal amplitudeMultiplier) { + if (!qFuzzyCompare(m_amplitudeMultiplier + 1.0, amplitudeMultiplier + 1.0)) { + m_amplitudeMultiplier = amplitudeMultiplier; + emit amplitudeMultiplierChanged(); + update(); + } +} + +int WavyLine::frequency() const { + return m_frequency; +} + +void WavyLine::setFrequency(int frequency) { + if (m_frequency != frequency) { + m_frequency = frequency; + emit frequencyChanged(); + update(); + } +} + +qreal WavyLine::startX() const { + return m_startX; +} + +void WavyLine::setStartX(qreal startX) { + if (!qFuzzyCompare(m_startX + 1.0, startX + 1.0)) { + m_startX = startX; + emit startXChanged(); + update(); + } +} + +qreal WavyLine::fullLength() const { + return m_fullLength; +} + +void WavyLine::setFullLength(qreal fullLength) { + if (!qFuzzyCompare(m_fullLength + 1.0, fullLength + 1.0)) { + m_fullLength = fullLength; + emit fullLengthChanged(); + update(); + } +} + +QColor WavyLine::color() const { + return m_color; +} + +void WavyLine::setColor(const QColor& color) { + if (m_color != color) { + m_color = color; + emit colorChanged(); + update(); + } +} + +qreal WavyLine::waveProgress() const { + return m_waveProgress; +} + +void WavyLine::setWaveProgress(qreal progress) { + if (!qFuzzyCompare(m_waveProgress + 1.0, progress + 1.0)) { + m_waveProgress = progress; + emit waveProgressChanged(); + update(); + } +} + +WavyLine::PathType WavyLine::pathType() const { + return m_pathType; +} + +void WavyLine::setPathType(PathType pathType) { + if (m_pathType != pathType) { + m_pathType = pathType; + emit pathTypeChanged(); + update(); + } +} + +qreal WavyLine::startAngle() const { + return m_startAngle; +} + +void WavyLine::setStartAngle(qreal startAngle) { + if (!qFuzzyCompare(m_startAngle + 1.0, startAngle + 1.0)) { + m_startAngle = startAngle; + m_startAngleRad = startAngle * M_PI / 180.0; + emit startAngleChanged(); + update(); + } +} + +qreal WavyLine::fullAngle() const { + return m_fullAngle; +} + +void WavyLine::setFullAngle(qreal fullAngle) { + if (!qFuzzyCompare(m_fullAngle + 1.0, fullAngle + 1.0)) { + m_fullAngle = fullAngle; + m_fullAngleRad = fullAngle * M_PI / 180.0; + emit fullAngleChanged(); + update(); + } +} + +qreal WavyLine::radius() const { + return m_radius; +} + +void WavyLine::setRadius(qreal radius) { + if (!qFuzzyCompare(m_radius + 1.0, radius + 1.0)) { + m_radius = radius; + emit radiusChanged(); + update(); + } +} + +qreal WavyLine::value() const { + return m_value; +} + +void WavyLine::setValue(qreal value) { + if (!qFuzzyCompare(m_value + 1.0, value + 1.0)) { + m_value = value; + emit valueChanged(); + update(); + } +} + +void WavyLine::paint(QPainter* painter) { + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(QPen(m_color, m_lineWidth, Qt::SolidLine, Qt::RoundCap)); + + if (m_pathType == Arc) { + paintArc(painter); + } else { + paintLinear(painter); + } +} + +void WavyLine::paintLinear(QPainter* painter) { + const auto amplitude = m_lineWidth * m_amplitudeMultiplier; + const auto phase = m_waveProgress * 2 * M_PI; + const auto centerY = height() / 2; + const auto len = m_fullLength > 0 ? m_fullLength : 1; + const auto start = m_lineWidth / 2.0; + const auto fullEnd = width() - m_lineWidth / 2.0; + const auto drawEnd = start + (fullEnd - start) * m_value; + + QPainterPath path; + bool first = true; + + for (int x = m_lineWidth / 2; x <= drawEnd; ++x) { + const auto theta = m_frequency * 2 * M_PI * (x + m_startX) / len + phase; + const auto waveY = centerY + amplitude * qSin(theta); + if (first) { + path.moveTo(x, waveY); + first = false; + } else { + path.lineTo(x, waveY); + } + } + + painter->drawPath(path); +} + +void WavyLine::paintArc(QPainter* painter) { + if (m_fullAngleRad <= 0) { + return; + } + + const auto amplitude = m_lineWidth * m_amplitudeMultiplier; + const auto cx = width() / 2.0; + const auto cy = height() / 2.0; + const auto radius = m_radius > 0 ? m_radius : (qMin(width(), height()) - m_lineWidth - 2 * amplitude) / 2.0; + + if (radius <= 0) { + return; + } + + const auto phase = m_waveProgress * 2 * M_PI; + const auto arcLen = radius * m_fullAngleRad; + const auto len = m_fullLength > 0 ? m_fullLength : arcLen; + const auto drawAngleRad = m_fullAngleRad * m_value; + + if (drawAngleRad <= 0) { + return; + } + + const auto N = qMax(64, qCeil(radius * drawAngleRad)); + const auto dTheta = drawAngleRad / N; + + QPainterPath path; + + for (int i = 0; i <= N; ++i) { + const auto theta = m_startAngleRad + i * dTheta; + const auto s = i * dTheta * radius; + const auto phi = m_frequency * 2 * M_PI * (s + m_startX) / len + phase; + const auto r = radius + amplitude * qSin(phi); + const auto px = cx + r * qCos(theta); + const auto py = cy + r * qSin(theta); + if (i == 0) { + path.moveTo(px, py); + } else { + path.lineTo(px, py); + } + } + + painter->drawPath(path); +} + +} // namespace caelestia::controls diff --git a/plugin/src/Caelestia/Components/wavyline.hpp b/plugin/src/Caelestia/Components/wavyline.hpp new file mode 100644 index 00000000..d0f865b6 --- /dev/null +++ b/plugin/src/Caelestia/Components/wavyline.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include +#include + +namespace caelestia::controls { + +class WavyLine : public QQuickPaintedItem { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(int lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged FINAL) + Q_PROPERTY(qreal amplitudeMultiplier READ amplitudeMultiplier WRITE setAmplitudeMultiplier NOTIFY + amplitudeMultiplierChanged FINAL) + Q_PROPERTY(int frequency READ frequency WRITE setFrequency NOTIFY frequencyChanged FINAL) + Q_PROPERTY(qreal startX READ startX WRITE setStartX NOTIFY startXChanged FINAL) + Q_PROPERTY(qreal fullLength READ fullLength WRITE setFullLength NOTIFY fullLengthChanged FINAL) + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL) + Q_PROPERTY(qreal waveProgress READ waveProgress WRITE setWaveProgress NOTIFY waveProgressChanged FINAL) + Q_PROPERTY(PathType pathType READ pathType WRITE setPathType NOTIFY pathTypeChanged FINAL) + Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged FINAL) + Q_PROPERTY(qreal fullAngle READ fullAngle WRITE setFullAngle NOTIFY fullAngleChanged FINAL) + Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged FINAL) + Q_PROPERTY(qreal value READ value WRITE setValue NOTIFY valueChanged FINAL) + +public: + enum PathType { + Linear, + Arc + }; + Q_ENUM(PathType) + + explicit WavyLine(QQuickItem* parent = nullptr); + + [[nodiscard]] int lineWidth() const; + void setLineWidth(int lineWidth); + + [[nodiscard]] qreal amplitudeMultiplier() const; + void setAmplitudeMultiplier(qreal amplitudeMultiplier); + + [[nodiscard]] int frequency() const; + void setFrequency(int frequency); + + [[nodiscard]] qreal startX() const; + void setStartX(qreal startX); + + [[nodiscard]] qreal fullLength() const; + void setFullLength(qreal fullLength); + + [[nodiscard]] QColor color() const; + void setColor(const QColor& color); + + [[nodiscard]] qreal waveProgress() const; + void setWaveProgress(qreal progress); + + [[nodiscard]] PathType pathType() const; + void setPathType(PathType pathType); + + [[nodiscard]] qreal startAngle() const; + void setStartAngle(qreal startAngle); + + [[nodiscard]] qreal fullAngle() const; + void setFullAngle(qreal fullAngle); + + [[nodiscard]] qreal radius() const; + void setRadius(qreal radius); + + [[nodiscard]] qreal value() const; + void setValue(qreal value); + + void paint(QPainter* painter) override; + +signals: + void lineWidthChanged(); + void amplitudeMultiplierChanged(); + void frequencyChanged(); + void startXChanged(); + void fullLengthChanged(); + void colorChanged(); + void waveProgressChanged(); + void pathTypeChanged(); + void startAngleChanged(); + void fullAngleChanged(); + void radiusChanged(); + void valueChanged(); + +private: + void paintLinear(QPainter* painter); + void paintArc(QPainter* painter); + + int m_lineWidth; + qreal m_amplitudeMultiplier; + int m_frequency; + qreal m_startX; + qreal m_fullLength; + QColor m_color; + qreal m_waveProgress; + PathType m_pathType; + qreal m_startAngle; + qreal m_fullAngle; + qreal m_radius; + qreal m_value; + qreal m_startAngleRad; + qreal m_fullAngleRad; +}; + +} // namespace caelestia::controls diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt index 4b26ea02..15e4b7c2 100644 --- a/plugin/src/Caelestia/Config/CMakeLists.txt +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -6,6 +6,8 @@ qml_module(caelestia-config configobject.cpp rootconfig.cpp appearanceconfig.cpp + font.cpp + fontbuilder.cpp tokens.cpp tokensattached.cpp anim.cpp @@ -13,11 +15,11 @@ qml_module(caelestia-config backgroundconfig.hpp barconfig.hpp borderconfig.hpp - controlcenterconfig.hpp dashboardconfig.hpp generalconfig.hpp launcherconfig.hpp lockconfig.hpp + nexusconfig.hpp notifsconfig.hpp osdconfig.hpp serviceconfig.hpp diff --git a/plugin/src/Caelestia/Config/appearanceconfig.cpp b/plugin/src/Caelestia/Config/appearanceconfig.cpp index 59228c67..a5230135 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.cpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.cpp @@ -36,14 +36,30 @@ int AppearanceRounding::small() const { return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; } -int AppearanceRounding::normal() const { - return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +int AppearanceRounding::medium() const { + return m_tokens ? static_cast(m_tokens->medium() * m_scale) : 0; } int AppearanceRounding::large() const { return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; } +int AppearanceRounding::largeIncreased() const { + return m_tokens ? static_cast(m_tokens->largeIncreased() * m_scale) : 0; +} + +int AppearanceRounding::extraLarge() const { + return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; +} + +int AppearanceRounding::extraLargeIncreased() const { + return m_tokens ? static_cast(m_tokens->extraLargeIncreased() * m_scale) : 0; +} + +int AppearanceRounding::extraExtraLarge() const { + return m_tokens ? static_cast(m_tokens->extraExtraLarge() * m_scale) : 0; +} + int AppearanceRounding::full() const { return m_tokens ? static_cast(m_tokens->full() * m_scale) : 0; } @@ -55,26 +71,38 @@ void AppearanceSpacing::bindTokens(SpacingTokens* tokens) { connectTokenSignals(tokens, this); } +int AppearanceSpacing::extraSmall() const { + return m_tokens ? static_cast(m_tokens->extraSmall() * m_scale) : 0; +} + int AppearanceSpacing::small() const { return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; } -int AppearanceSpacing::smaller() const { - return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; -} - -int AppearanceSpacing::normal() const { - return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; -} - -int AppearanceSpacing::larger() const { - return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; +int AppearanceSpacing::medium() const { + return m_tokens ? static_cast(m_tokens->medium() * m_scale) : 0; } int AppearanceSpacing::large() const { return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; } +int AppearanceSpacing::largeIncreased() const { + return m_tokens ? static_cast(m_tokens->largeIncreased() * m_scale) : 0; +} + +int AppearanceSpacing::extraLarge() const { + return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; +} + +int AppearanceSpacing::extraLargeIncreased() const { + return m_tokens ? static_cast(m_tokens->extraLargeIncreased() * m_scale) : 0; +} + +int AppearanceSpacing::extraExtraLarge() const { + return m_tokens ? static_cast(m_tokens->extraExtraLarge() * m_scale) : 0; +} + // AppearancePadding void AppearancePadding::bindTokens(PaddingTokens* tokens) { @@ -82,57 +110,52 @@ void AppearancePadding::bindTokens(PaddingTokens* tokens) { connectTokenSignals(tokens, this); } +int AppearancePadding::extraSmall() const { + return m_tokens ? static_cast(m_tokens->extraSmall() * m_scale) : 0; +} + int AppearancePadding::small() const { return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; } -int AppearancePadding::smaller() const { - return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; -} - -int AppearancePadding::normal() const { - return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; -} - -int AppearancePadding::larger() const { - return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; +int AppearancePadding::medium() const { + return m_tokens ? static_cast(m_tokens->medium() * m_scale) : 0; } int AppearancePadding::large() const { return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; } -// FontSize - -void FontSize::bindTokens(FontSizeTokens* tokens) { - m_tokens = tokens; - connectTokenSignals(tokens, this); +int AppearancePadding::largeIncreased() const { + return m_tokens ? static_cast(m_tokens->largeIncreased() * m_scale) : 0; } -int FontSize::small() const { - return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; -} - -int FontSize::smaller() const { - return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; -} - -int FontSize::normal() const { - return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; -} - -int FontSize::larger() const { - return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; -} - -int FontSize::large() const { - return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; -} - -int FontSize::extraLarge() const { +int AppearancePadding::extraLarge() const { return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; } +int AppearancePadding::extraLargeIncreased() const { + return m_tokens ? static_cast(m_tokens->extraLargeIncreased() * m_scale) : 0; +} + +int AppearancePadding::extraExtraLarge() const { + return m_tokens ? static_cast(m_tokens->extraExtraLarge() * m_scale) : 0; +} + +// FontConfig + +void FontConfig::setDefaults(int size, int weight, const QVariantMap& vaxes) { + m_size = size; + m_weight = weight; + m_vaxes = vaxes; +} + +// FontStyleConfig + +void FontStyleConfig::setDefaultFamily(const QString& family) { + m_family = family; +} + // AnimDurations void AnimDurations::bindTokens(AnimDurationTokens* tokens) { diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp index 3446ea72..17e18aa1 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.hpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -2,7 +2,9 @@ #include "configobject.hpp" +#include #include +#include namespace caelestia::config { @@ -10,7 +12,6 @@ namespace caelestia::config { class RoundingTokens; class SpacingTokens; class PaddingTokens; -class FontSizeTokens; class AnimDurationTokens; class AppearanceRounding : public ConfigObject { @@ -21,8 +22,12 @@ class AppearanceRounding : public ConfigObject { Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged) Q_PROPERTY(int small READ small NOTIFY valuesChanged) - Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int medium READ medium NOTIFY valuesChanged) Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) + Q_PROPERTY(int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged) Q_PROPERTY(int full READ full NOTIFY valuesChanged) public: @@ -33,8 +38,12 @@ public: [[nodiscard]] int extraSmall() const; [[nodiscard]] int small() const; - [[nodiscard]] int normal() const; + [[nodiscard]] int medium() const; [[nodiscard]] int large() const; + [[nodiscard]] int largeIncreased() const; + [[nodiscard]] int extraLarge() const; + [[nodiscard]] int extraLargeIncreased() const; + [[nodiscard]] int extraExtraLarge() const; [[nodiscard]] int full() const; signals: @@ -50,11 +59,14 @@ class AppearanceSpacing : public ConfigObject { CONFIG_PROPERTY(qreal, scale, 1) + Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged) Q_PROPERTY(int small READ small NOTIFY valuesChanged) - Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) - Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) - Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) + Q_PROPERTY(int medium READ medium NOTIFY valuesChanged) Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) + Q_PROPERTY(int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged) public: explicit AppearanceSpacing(QObject* parent = nullptr) @@ -62,11 +74,14 @@ public: void bindTokens(SpacingTokens* tokens); + [[nodiscard]] int extraSmall() const; [[nodiscard]] int small() const; - [[nodiscard]] int smaller() const; - [[nodiscard]] int normal() const; - [[nodiscard]] int larger() const; + [[nodiscard]] int medium() const; [[nodiscard]] int large() const; + [[nodiscard]] int largeIncreased() const; + [[nodiscard]] int extraLarge() const; + [[nodiscard]] int extraLargeIncreased() const; + [[nodiscard]] int extraExtraLarge() const; signals: void valuesChanged(); @@ -81,11 +96,14 @@ class AppearancePadding : public ConfigObject { CONFIG_PROPERTY(qreal, scale, 1) + Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged) Q_PROPERTY(int small READ small NOTIFY valuesChanged) - Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) - Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) - Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) + Q_PROPERTY(int medium READ medium NOTIFY valuesChanged) Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) + Q_PROPERTY(int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged) public: explicit AppearancePadding(QObject* parent = nullptr) @@ -93,11 +111,14 @@ public: void bindTokens(PaddingTokens* tokens); + [[nodiscard]] int extraSmall() const; [[nodiscard]] int small() const; - [[nodiscard]] int smaller() const; - [[nodiscard]] int normal() const; - [[nodiscard]] int larger() const; + [[nodiscard]] int medium() const; [[nodiscard]] int large() const; + [[nodiscard]] int largeIncreased() const; + [[nodiscard]] int extraLarge() const; + [[nodiscard]] int extraLargeIncreased() const; + [[nodiscard]] int extraExtraLarge() const; signals: void valuesChanged(); @@ -106,65 +127,113 @@ private: PaddingTokens* m_tokens = nullptr; }; -class FontFamily : public ConfigObject { +class FontConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(QString, sans, QStringLiteral("Rubik")) - CONFIG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF")) - CONFIG_PROPERTY(QString, material, QStringLiteral("Material Symbols Rounded")) - CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik")) + // Empty family inherits from the parent FontStyleConfig. + CONFIG_PROPERTY(QString, family, {}) + CONFIG_PROPERTY(int, size, 14) + CONFIG_PROPERTY(int, weight, QFont::Normal) + CONFIG_PROPERTY(bool, italic, false) + CONFIG_PROPERTY(QVariantMap, vaxes, {}) public: - explicit FontFamily(QObject* parent = nullptr) + explicit FontConfig(QObject* parent = nullptr) : ConfigObject(parent) {} + + void setDefaults(int size, int weight = QFont::Normal, const QVariantMap& vaxes = {}); }; -class FontSize : public ConfigObject { +class FontStyleConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(qreal, scale, 1) - - Q_PROPERTY(int small READ small NOTIFY valuesChanged) - Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) - Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) - Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) - Q_PROPERTY(int large READ large NOTIFY valuesChanged) - Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) + CONFIG_PROPERTY(QString, family, QStringLiteral("GoogleSansFlex")) + CONFIG_SUBOBJECT(FontConfig, large) + CONFIG_SUBOBJECT(FontConfig, medium) + CONFIG_SUBOBJECT(FontConfig, small) public: - explicit FontSize(QObject* parent = nullptr) - : ConfigObject(parent) {} + explicit FontStyleConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_large(new FontConfig(this)) + , m_medium(new FontConfig(this)) + , m_small(new FontConfig(this)) {} - void bindTokens(FontSizeTokens* tokens); + void setDefaultFamily(const QString& family); +}; - [[nodiscard]] int small() const; - [[nodiscard]] int smaller() const; - [[nodiscard]] int normal() const; - [[nodiscard]] int larger() const; - [[nodiscard]] int large() const; - [[nodiscard]] int extraLarge() const; +class IconFontStyleConfig : public FontStyleConfig { + Q_OBJECT + QML_ANONYMOUS -signals: - void valuesChanged(); + CONFIG_SUBOBJECT(FontConfig, extraLarge) -private: - FontSizeTokens* m_tokens = nullptr; +public: + explicit IconFontStyleConfig(QObject* parent = nullptr) + : FontStyleConfig(parent) + , m_extraLarge(new FontConfig(this)) {} }; class AppearanceFont : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_SUBOBJECT(FontFamily, family) - CONFIG_SUBOBJECT(FontSize, size) + CONFIG_PROPERTY(qreal, scale, 1) + CONFIG_SUBOBJECT(FontStyleConfig, headline) + CONFIG_SUBOBJECT(FontStyleConfig, title) + CONFIG_SUBOBJECT(FontStyleConfig, body) + CONFIG_SUBOBJECT(FontStyleConfig, label) + CONFIG_SUBOBJECT(FontStyleConfig, mono) + CONFIG_SUBOBJECT(IconFontStyleConfig, icon) + CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik")) public: explicit AppearanceFont(QObject* parent = nullptr) : ConfigObject(parent) - , m_family(new FontFamily(this)) - , m_size(new FontSize(this)) {} + , m_headline(new FontStyleConfig(this)) + , m_title(new FontStyleConfig(this)) + , m_body(new FontStyleConfig(this)) + , m_label(new FontStyleConfig(this)) + , m_mono(new FontStyleConfig(this)) + , m_icon(new IconFontStyleConfig(this)) { + const auto sans = QStringLiteral("GoogleSansFlex"); + const auto mono = QStringLiteral("CaskaydiaCove NF"); + const auto icons = QStringLiteral("Material Symbols Rounded"); + const QVariantMap vaxes = { { "ROND", 25 } }; + + m_headline->setDefaultFamily(sans); + m_headline->large()->setDefaults(32, QFont::Medium, vaxes); + m_headline->medium()->setDefaults(28, QFont::Medium, vaxes); + m_headline->small()->setDefaults(24, QFont::Medium, vaxes); + + m_title->setDefaultFamily(sans); + m_title->large()->setDefaults(22, QFont::Medium, vaxes); + m_title->medium()->setDefaults(16, QFont::Medium, vaxes); + m_title->small()->setDefaults(14, QFont::Medium, vaxes); + + m_body->setDefaultFamily(sans); + m_body->large()->setDefaults(16, QFont::Normal, vaxes); + m_body->medium()->setDefaults(14, QFont::Normal, vaxes); + m_body->small()->setDefaults(12, QFont::Normal, vaxes); + + m_label->setDefaultFamily(sans); + m_label->large()->setDefaults(14, QFont::Medium, vaxes); + m_label->medium()->setDefaults(12, QFont::Medium, vaxes); + m_label->small()->setDefaults(11, QFont::Normal, vaxes); + + m_mono->setDefaultFamily(mono); + m_mono->large()->setDefaults(16, QFont::Normal); + m_mono->medium()->setDefaults(14, QFont::Normal); + m_mono->small()->setDefaults(12, QFont::Normal); + + m_icon->setDefaultFamily(icons); + m_icon->extraLarge()->setDefaults(static_cast(48 / 1.33), QFont::Normal); + m_icon->large()->setDefaults(static_cast(32 / 1.33), QFont::Normal); + m_icon->medium()->setDefaults(static_cast(24 / 1.33), QFont::Normal); + m_icon->small()->setDefaults(static_cast(20 / 1.33), QFont::Normal); + } }; class AnimDurations : public ConfigObject { diff --git a/plugin/src/Caelestia/Config/borderconfig.hpp b/plugin/src/Caelestia/Config/borderconfig.hpp index 9de604e8..f2482563 100644 --- a/plugin/src/Caelestia/Config/borderconfig.hpp +++ b/plugin/src/Caelestia/Config/borderconfig.hpp @@ -12,7 +12,7 @@ class BorderConfig : public ConfigObject { CONFIG_PROPERTY(int, thickness, 10) CONFIG_PROPERTY(int, rounding, 25) - CONFIG_PROPERTY(int, smoothing, 32) + CONFIG_PROPERTY(int, smoothing, 20) Q_PROPERTY(int minThickness READ minThickness CONSTANT) Q_PROPERTY(int clampedThickness READ clampedThickness NOTIFY thicknessChanged) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index ff939f99..b09ddb83 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -3,12 +3,12 @@ #include "backgroundconfig.hpp" #include "barconfig.hpp" #include "borderconfig.hpp" -#include "controlcenterconfig.hpp" #include "dashboardconfig.hpp" #include "generalconfig.hpp" #include "launcherconfig.hpp" #include "lockconfig.hpp" #include "monitorconfigmanager.hpp" +#include "nexusconfig.hpp" #include "notifsconfig.hpp" #include "osdconfig.hpp" #include "serviceconfig.hpp" @@ -40,16 +40,16 @@ GlobalConfig::GlobalConfig(QObject* parent) , m_bar(new BarConfig(this)) , m_border(new BorderConfig(this)) , m_dashboard(new DashboardConfig(this)) - , m_controlCenter(new ControlCenterConfig(this)) , m_launcher(new LauncherConfig(this)) + , m_lock(new LockConfig(this)) + , m_nexus(new NexusConfig(this)) , m_notifs(new NotifsConfig(this)) , m_osd(new OsdConfig(this)) - , m_session(new SessionConfig(this)) - , m_winfo(new WInfoConfig(this)) - , m_lock(new LockConfig(this)) - , m_utilities(new UtilitiesConfig(this)) - , m_sidebar(new SidebarConfig(this)) , m_services(new ServiceConfig(this)) + , m_session(new SessionConfig(this)) + , m_sidebar(new SidebarConfig(this)) + , m_utilities(new UtilitiesConfig(this)) + , m_winfo(new WInfoConfig(this)) , m_paths(new UserPaths(this)) { setupFileBackend(configDir() + QStringLiteral("shell.json")); } @@ -62,16 +62,16 @@ GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, cons , m_bar(new BarConfig(this)) , m_border(new BorderConfig(this)) , m_dashboard(new DashboardConfig(this)) - , m_controlCenter(new ControlCenterConfig(this)) , m_launcher(new LauncherConfig(this)) + , m_lock(new LockConfig(this)) + , m_nexus(new NexusConfig(this)) , m_notifs(new NotifsConfig(this)) , m_osd(new OsdConfig(this)) - , m_session(new SessionConfig(this)) - , m_winfo(new WInfoConfig(this)) - , m_lock(new LockConfig(this)) - , m_utilities(new UtilitiesConfig(this)) - , m_sidebar(new SidebarConfig(this)) , m_services(new ServiceConfig(this)) + , m_session(new SessionConfig(this)) + , m_sidebar(new SidebarConfig(this)) + , m_utilities(new UtilitiesConfig(this)) + , m_winfo(new WInfoConfig(this)) , m_paths(new UserPaths(this)) { if (!filePath.isEmpty()) setupFileBackend(filePath, screen); @@ -103,7 +103,6 @@ void GlobalConfig::bindAppearanceTokens() { m_appearance->rounding()->bindTokens(tokenAppearance->rounding()); m_appearance->spacing()->bindTokens(tokenAppearance->spacing()); m_appearance->padding()->bindTokens(tokenAppearance->padding()); - m_appearance->font()->size()->bindTokens(tokenAppearance->fontSize()); m_appearance->anim()->durations()->bindTokens(tokenAppearance->animDurations()); m_tokensBound = true; } diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index 248a3f22..1418e980 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -10,11 +10,11 @@ class AppearanceConfig; class BackgroundConfig; class BarConfig; class BorderConfig; -class ControlCenterConfig; class DashboardConfig; class GeneralConfig; class LauncherConfig; class LockConfig; +class NexusConfig; class NotifsConfig; class OsdConfig; class ServiceConfig; @@ -32,11 +32,11 @@ class GlobalConfig : public RootConfig { Q_MOC_INCLUDE("backgroundconfig.hpp") Q_MOC_INCLUDE("barconfig.hpp") Q_MOC_INCLUDE("borderconfig.hpp") - Q_MOC_INCLUDE("controlcenterconfig.hpp") Q_MOC_INCLUDE("dashboardconfig.hpp") Q_MOC_INCLUDE("generalconfig.hpp") Q_MOC_INCLUDE("launcherconfig.hpp") Q_MOC_INCLUDE("lockconfig.hpp") + Q_MOC_INCLUDE("nexusconfig.hpp") Q_MOC_INCLUDE("notifsconfig.hpp") Q_MOC_INCLUDE("osdconfig.hpp") Q_MOC_INCLUDE("serviceconfig.hpp") @@ -53,16 +53,16 @@ class GlobalConfig : public RootConfig { CONFIG_SUBOBJECT(BarConfig, bar) CONFIG_SUBOBJECT(BorderConfig, border) CONFIG_SUBOBJECT(DashboardConfig, dashboard) - CONFIG_SUBOBJECT(ControlCenterConfig, controlCenter) CONFIG_SUBOBJECT(LauncherConfig, launcher) + CONFIG_SUBOBJECT(LockConfig, lock) + CONFIG_SUBOBJECT(NexusConfig, nexus) CONFIG_SUBOBJECT(NotifsConfig, notifs) CONFIG_SUBOBJECT(OsdConfig, osd) - CONFIG_SUBOBJECT(SessionConfig, session) - CONFIG_SUBOBJECT(WInfoConfig, winfo) - CONFIG_SUBOBJECT(LockConfig, lock) - CONFIG_SUBOBJECT(UtilitiesConfig, utilities) - CONFIG_SUBOBJECT(SidebarConfig, sidebar) CONFIG_SUBOBJECT(ServiceConfig, services) + CONFIG_SUBOBJECT(SessionConfig, session) + CONFIG_SUBOBJECT(SidebarConfig, sidebar) + CONFIG_SUBOBJECT(UtilitiesConfig, utilities) + CONFIG_SUBOBJECT(WInfoConfig, winfo) CONFIG_SUBOBJECT(UserPaths, paths) public: diff --git a/plugin/src/Caelestia/Config/configattached.cpp b/plugin/src/Caelestia/Config/configattached.cpp index 8d229049..38901470 100644 --- a/plugin/src/Caelestia/Config/configattached.cpp +++ b/plugin/src/Caelestia/Config/configattached.cpp @@ -71,16 +71,16 @@ CONFIG_ATTACHED_GETTER(BackgroundConfig, background) CONFIG_ATTACHED_GETTER(BarConfig, bar) CONFIG_ATTACHED_GETTER(BorderConfig, border) CONFIG_ATTACHED_GETTER(DashboardConfig, dashboard) -CONFIG_ATTACHED_GETTER(ControlCenterConfig, controlCenter) CONFIG_ATTACHED_GETTER(LauncherConfig, launcher) +CONFIG_ATTACHED_GETTER(LockConfig, lock) +CONFIG_ATTACHED_GETTER(NexusConfig, nexus) CONFIG_ATTACHED_GETTER(NotifsConfig, notifs) CONFIG_ATTACHED_GETTER(OsdConfig, osd) -CONFIG_ATTACHED_GETTER(SessionConfig, session) -CONFIG_ATTACHED_GETTER(WInfoConfig, winfo) -CONFIG_ATTACHED_GETTER(LockConfig, lock) -CONFIG_ATTACHED_GETTER(UtilitiesConfig, utilities) -CONFIG_ATTACHED_GETTER(SidebarConfig, sidebar) CONFIG_ATTACHED_GETTER(ServiceConfig, services) +CONFIG_ATTACHED_GETTER(SessionConfig, session) +CONFIG_ATTACHED_GETTER(SidebarConfig, sidebar) +CONFIG_ATTACHED_GETTER(UtilitiesConfig, utilities) +CONFIG_ATTACHED_GETTER(WInfoConfig, winfo) CONFIG_ATTACHED_GETTER(UserPaths, paths) #undef CONFIG_ATTACHED_GETTER diff --git a/plugin/src/Caelestia/Config/configattached.hpp b/plugin/src/Caelestia/Config/configattached.hpp index 815b1080..13f3bf6c 100644 --- a/plugin/src/Caelestia/Config/configattached.hpp +++ b/plugin/src/Caelestia/Config/configattached.hpp @@ -16,11 +16,11 @@ class Config : public QQuickAttachedPropertyPropagator, public QQmlParserStatus Q_MOC_INCLUDE("backgroundconfig.hpp") Q_MOC_INCLUDE("barconfig.hpp") Q_MOC_INCLUDE("borderconfig.hpp") - Q_MOC_INCLUDE("controlcenterconfig.hpp") Q_MOC_INCLUDE("dashboardconfig.hpp") Q_MOC_INCLUDE("generalconfig.hpp") Q_MOC_INCLUDE("launcherconfig.hpp") Q_MOC_INCLUDE("lockconfig.hpp") + Q_MOC_INCLUDE("nexusconfig.hpp") Q_MOC_INCLUDE("notifsconfig.hpp") Q_MOC_INCLUDE("osdconfig.hpp") Q_MOC_INCLUDE("serviceconfig.hpp") @@ -37,16 +37,16 @@ class Config : public QQuickAttachedPropertyPropagator, public QQmlParserStatus Q_PROPERTY(const caelestia::config::BarConfig* bar READ bar NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::BorderConfig* border READ border NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::DashboardConfig* dashboard READ dashboard NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::ControlCenterConfig* controlCenter READ controlCenter NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::LauncherConfig* launcher READ launcher NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::LockConfig* lock READ lock NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::NexusConfig* nexus READ nexus NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::NotifsConfig* notifs READ notifs NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::OsdConfig* osd READ osd NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::SessionConfig* session READ session NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::WInfoConfig* winfo READ winfo NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::LockConfig* lock READ lock NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::ServiceConfig* services READ services NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SessionConfig* session READ session NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::WInfoConfig* winfo READ winfo NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::UserPaths* paths READ paths NOTIFY sourceChanged) public: @@ -61,16 +61,16 @@ public: [[nodiscard]] const BarConfig* bar() const; [[nodiscard]] const BorderConfig* border() const; [[nodiscard]] const DashboardConfig* dashboard() const; - [[nodiscard]] const ControlCenterConfig* controlCenter() const; [[nodiscard]] const LauncherConfig* launcher() const; + [[nodiscard]] const LockConfig* lock() const; + [[nodiscard]] const NexusConfig* nexus() const; [[nodiscard]] const NotifsConfig* notifs() const; [[nodiscard]] const OsdConfig* osd() const; - [[nodiscard]] const SessionConfig* session() const; - [[nodiscard]] const WInfoConfig* winfo() const; - [[nodiscard]] const LockConfig* lock() const; - [[nodiscard]] const UtilitiesConfig* utilities() const; - [[nodiscard]] const SidebarConfig* sidebar() const; [[nodiscard]] const ServiceConfig* services() const; + [[nodiscard]] const SessionConfig* session() const; + [[nodiscard]] const SidebarConfig* sidebar() const; + [[nodiscard]] const UtilitiesConfig* utilities() const; + [[nodiscard]] const WInfoConfig* winfo() const; [[nodiscard]] const UserPaths* paths() const; [[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen); diff --git a/plugin/src/Caelestia/Config/controlcenterconfig.hpp b/plugin/src/Caelestia/Config/controlcenterconfig.hpp deleted file mode 100644 index 80b40b87..00000000 --- a/plugin/src/Caelestia/Config/controlcenterconfig.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "configobject.hpp" - -namespace caelestia::config { - -// ControlCenterConfig has no serialized properties (serializer returns {}) -// All properties are in AdvancedConfig.controlCenter -class ControlCenterConfig : public ConfigObject { - Q_OBJECT - QML_ANONYMOUS - -public: - explicit ControlCenterConfig(QObject* parent = nullptr) - : ConfigObject(parent) {} -}; - -} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/font.cpp b/plugin/src/Caelestia/Config/font.cpp new file mode 100644 index 00000000..edd75069 --- /dev/null +++ b/plugin/src/Caelestia/Config/font.cpp @@ -0,0 +1,263 @@ +#include "font.hpp" +#include "appearanceconfig.hpp" + +namespace caelestia::config { + +// FontStyleBase + +QFont FontStyleBase::large() const { + return m_large; +} + +QFont FontStyleBase::medium() const { + return m_medium; +} + +QFont FontStyleBase::small() const { + return m_small; +} + +QFont FontStyleBase::buildFont(const FontConfig* cfg, const QString& fallbackFamily, qreal scale) { + QFont font; + font.setFamily(cfg->family().isEmpty() ? fallbackFamily : cfg->family()); + const int scaledSize = static_cast(cfg->size() * scale); + const int cappedSize = scaledSize > 0 ? scaledSize : 1; + font.setPointSize(cappedSize); + font.setVariableAxis("opsz", static_cast(cappedSize)); + font.setWeight(QFont::Weight(cfg->weight())); + font.setVariableAxis("wght", font.weight()); + font.setItalic(cfg->italic()); + + const auto axes = cfg->vaxes(); + for (auto it = axes.constBegin(); it != axes.constEnd(); ++it) { + if (auto tag = QFont::Tag::fromString(it.key())) + font.setVariableAxis(*tag, it.value().toFloat()); + } + + return font; +} + +void FontStyleBase::bind(FontStyleConfig* cfg) { + if (m_cfg == cfg) + return; + + if (m_cfg) { + disconnect(m_cfg, nullptr, this, nullptr); + disconnect(m_cfg->large(), nullptr, this, nullptr); + disconnect(m_cfg->medium(), nullptr, this, nullptr); + disconnect(m_cfg->small(), nullptr, this, nullptr); + } + + m_cfg = cfg; + + if (cfg) { + connect(cfg, &ConfigObject::propertiesChanged, this, &FontStyleBase::rebuild); + connect(cfg->large(), &ConfigObject::propertiesChanged, this, &FontStyleBase::rebuild); + connect(cfg->medium(), &ConfigObject::propertiesChanged, this, &FontStyleBase::rebuild); + connect(cfg->small(), &ConfigObject::propertiesChanged, this, &FontStyleBase::rebuild); + } + + rebuild(); +} + +void FontStyleBase::rebuild() { + if (m_cfg) { + const auto family = m_cfg->family(); + m_large = buildFont(m_cfg->large(), family, m_scale); + m_medium = buildFont(m_cfg->medium(), family, m_scale); + m_small = buildFont(m_cfg->small(), family, m_scale); + } else { + m_large = QFont(); + m_medium = QFont(); + m_small = QFont(); + } + emit fontsChanged(); +} + +void FontStyleBase::setScale(qreal scale) { + if (qFuzzyCompare(m_scale + 1.0, scale + 1.0)) + return; + m_scale = scale; + rebuild(); +} + +// FontStyle + +FontStyle::FontStyle(QObject* parent) + : FontStyleBase(parent) + , m_builders(new FontBuilders(this, this)) {} + +FontBuilders* FontStyle::builders() const { + return m_builders; +} + +// IconFontStyle + +IconFontStyle::IconFontStyle(QObject* parent) + : FontStyleBase(parent) + , m_builders(new IconFontBuilders(this, this)) {} + +FontBuilder IconFontStyle::size(int pointSize) { + return FontBuilder(m_small).size(pointSize); +} + +void IconFontStyle::bind(IconFontStyleConfig* cfg) { + if (m_cfg == cfg) + return; + + if (m_cfg) { + // Previous cfg may or may not be an IconFontStyleConfig; guard via cast. + if (auto* prev = qobject_cast(m_cfg)) + disconnect(prev->extraLarge(), nullptr, this, nullptr); + } + + FontStyleBase::bind(cfg); + + if (cfg) + connect(cfg->extraLarge(), &ConfigObject::propertiesChanged, this, &IconFontStyle::rebuild); +} + +QFont IconFontStyle::extraLarge() const { + return m_extraLarge; +} + +IconFontBuilders* IconFontStyle::builders() const { + return m_builders; +} + +void IconFontStyle::rebuild() { + if (auto* cfg = qobject_cast(m_cfg)) { + const auto family = cfg->family(); + m_large = buildFont(cfg->large(), family, m_scale); + m_medium = buildFont(cfg->medium(), family, m_scale); + m_small = buildFont(cfg->small(), family, m_scale); + m_extraLarge = buildFont(cfg->extraLarge(), family, m_scale); + } else { + m_large = QFont(); + m_medium = QFont(); + m_small = QFont(); + m_extraLarge = QFont(); + } + emit fontsChanged(); +} + +// FontBuilders + +FontBuilders::FontBuilders(const FontStyleBase* style, QObject* parent) + : QObject(parent) + , m_style(style) { + connect(style, &FontStyleBase::fontsChanged, this, &FontBuilders::buildersChanged); +} + +FontBuilder FontBuilders::large() const { + return FontBuilder(m_style->large()); +} + +FontBuilder FontBuilders::medium() const { + return FontBuilder(m_style->medium()); +} + +FontBuilder FontBuilders::small() const { + return FontBuilder(m_style->small()); +} + +// IconFontBuilders + +IconFontBuilders::IconFontBuilders(const IconFontStyle* style, QObject* parent) + : FontBuilders(style, parent) {} + +FontBuilder IconFontBuilders::extraLarge() const { + return FontBuilder(static_cast(m_style)->extraLarge()); +} + +// FontTokens + +FontTokens::FontTokens(QObject* parent) + : QObject(parent) + , m_headline(new FontStyle(this)) + , m_title(new FontStyle(this)) + , m_body(new FontStyle(this)) + , m_label(new FontStyle(this)) + , m_mono(new FontStyle(this)) + , m_icon(new IconFontStyle(this)) {} + +FontStyle* FontTokens::headline() const { + return m_headline; +} + +FontStyle* FontTokens::title() const { + return m_title; +} + +FontStyle* FontTokens::body() const { + return m_body; +} + +FontStyle* FontTokens::label() const { + return m_label; +} + +FontStyle* FontTokens::mono() const { + return m_mono; +} + +IconFontStyle* FontTokens::icon() const { + return m_icon; +} + +FontBuilder FontTokens::clock() const { + return FontBuilder(m_clock); +} + +void FontTokens::bindFont(AppearanceFont* font) { + if (m_font == font) + return; + + if (m_font) + disconnect(m_font, nullptr, this, nullptr); + + m_font = font; + + if (font) { + rebuildScale(); + m_headline->bind(font->headline()); + m_title->bind(font->title()); + m_body->bind(font->body()); + m_label->bind(font->label()); + m_mono->bind(font->mono()); + m_icon->bind(font->icon()); + + connect(font, &AppearanceFont::clockChanged, this, &FontTokens::rebuildClock); + connect(font, &AppearanceFont::scaleChanged, this, &FontTokens::rebuildScale); + } else { + rebuildScale(); + m_headline->bind(nullptr); + m_title->bind(nullptr); + m_body->bind(nullptr); + m_label->bind(nullptr); + m_mono->bind(nullptr); + m_icon->bind(nullptr); + } + + rebuildClock(); +} + +void FontTokens::rebuildScale() { + const qreal s = m_font ? m_font->scale() : 1; + m_headline->setScale(s); + m_title->setScale(s); + m_body->setScale(s); + m_label->setScale(s); + m_mono->setScale(s); + m_icon->setScale(s); +} + +void FontTokens::rebuildClock() { + QFont f; + if (m_font) + f.setFamily(m_font->clock()); + m_clock = f; + emit clockChanged(); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/font.hpp b/plugin/src/Caelestia/Config/font.hpp new file mode 100644 index 00000000..2cfbf341 --- /dev/null +++ b/plugin/src/Caelestia/Config/font.hpp @@ -0,0 +1,165 @@ +#pragma once + +#include "fontbuilder.hpp" + +#include + +namespace caelestia::config { + +class AppearanceFont; +class FontConfig; +class FontStyleConfig; +class IconFontStyleConfig; +class FontStyleBase; +class IconFontStyle; + +class FontBuilders : public QObject { + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(caelestia::config::FontBuilder large READ large NOTIFY buildersChanged FINAL) + Q_PROPERTY(caelestia::config::FontBuilder medium READ medium NOTIFY buildersChanged FINAL) + Q_PROPERTY(caelestia::config::FontBuilder small READ small NOTIFY buildersChanged FINAL) + +public: + explicit FontBuilders(const FontStyleBase* style, QObject* parent = nullptr); + + [[nodiscard]] FontBuilder large() const; + [[nodiscard]] FontBuilder medium() const; + [[nodiscard]] FontBuilder small() const; + +signals: + void buildersChanged(); + +protected: + const FontStyleBase* m_style; +}; + +class IconFontBuilders : public FontBuilders { + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(caelestia::config::FontBuilder extraLarge READ extraLarge NOTIFY buildersChanged FINAL) + +public: + explicit IconFontBuilders(const IconFontStyle* style, QObject* parent = nullptr); + + [[nodiscard]] FontBuilder extraLarge() const; +}; + +class FontStyleBase : public QObject { + Q_OBJECT + + Q_PROPERTY(QFont large READ large NOTIFY fontsChanged FINAL) + Q_PROPERTY(QFont medium READ medium NOTIFY fontsChanged FINAL) + Q_PROPERTY(QFont small READ small NOTIFY fontsChanged FINAL) + +public: + explicit FontStyleBase(QObject* parent = nullptr) + : QObject(parent) {} + + void bind(FontStyleConfig* cfg); + void setScale(qreal scale); + + [[nodiscard]] QFont large() const; + [[nodiscard]] QFont medium() const; + [[nodiscard]] QFont small() const; + +signals: + void fontsChanged(); + +protected: + virtual void rebuild(); + + static QFont buildFont(const FontConfig* cfg, const QString& fallbackFamily, qreal scale); + + FontStyleConfig* m_cfg = nullptr; + qreal m_scale = 1; + QFont m_large; + QFont m_medium; + QFont m_small; +}; + +class FontStyle : public FontStyleBase { + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(caelestia::config::FontBuilders* builders READ builders CONSTANT FINAL) + +public: + explicit FontStyle(QObject* parent = nullptr); + + [[nodiscard]] FontBuilders* builders() const; + +private: + FontBuilders* m_builders; +}; + +class IconFontStyle : public FontStyleBase { + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(QFont extraLarge READ extraLarge NOTIFY fontsChanged FINAL) + Q_PROPERTY(caelestia::config::IconFontBuilders* builders READ builders CONSTANT FINAL) + +public: + explicit IconFontStyle(QObject* parent = nullptr); + + Q_INVOKABLE FontBuilder size(int pointSize); + + void bind(IconFontStyleConfig* cfg); + + [[nodiscard]] QFont extraLarge() const; + [[nodiscard]] IconFontBuilders* builders() const; + +protected: + void rebuild() override; + +private: + QFont m_extraLarge; + IconFontBuilders* m_builders; +}; + +class FontTokens : public QObject { + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(caelestia::config::FontStyle* headline READ headline CONSTANT FINAL) + Q_PROPERTY(caelestia::config::FontStyle* title READ title CONSTANT FINAL) + Q_PROPERTY(caelestia::config::FontStyle* body READ body CONSTANT FINAL) + Q_PROPERTY(caelestia::config::FontStyle* label READ label CONSTANT FINAL) + Q_PROPERTY(caelestia::config::FontStyle* mono READ mono CONSTANT FINAL) + Q_PROPERTY(caelestia::config::IconFontStyle* icon READ icon CONSTANT FINAL) + Q_PROPERTY(caelestia::config::FontBuilder clock READ clock NOTIFY clockChanged FINAL) + +public: + explicit FontTokens(QObject* parent = nullptr); + + void bindFont(AppearanceFont* font); + + [[nodiscard]] FontStyle* headline() const; + [[nodiscard]] FontStyle* title() const; + [[nodiscard]] FontStyle* body() const; + [[nodiscard]] FontStyle* label() const; + [[nodiscard]] FontStyle* mono() const; + [[nodiscard]] IconFontStyle* icon() const; + [[nodiscard]] FontBuilder clock() const; + +signals: + void clockChanged(); + +private: + void rebuildClock(); + void rebuildScale(); + + AppearanceFont* m_font = nullptr; + FontStyle* m_headline; + FontStyle* m_title; + FontStyle* m_body; + FontStyle* m_label; + FontStyle* m_mono; + IconFontStyle* m_icon; + QFont m_clock; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/fontbuilder.cpp b/plugin/src/Caelestia/Config/fontbuilder.cpp new file mode 100644 index 00000000..2d2e3fd9 --- /dev/null +++ b/plugin/src/Caelestia/Config/fontbuilder.cpp @@ -0,0 +1,92 @@ +#include "fontbuilder.hpp" +#include + +namespace caelestia::config { + +Q_LOGGING_CATEGORY(lcFontBuilder, "caelestia.fontbuilder", QtInfoMsg) + +FontBuilder::FontBuilder(QFont font) + : m_font(std::move(font)) {} + +FontBuilder FontBuilder::family(const QString& family) { + m_font.setFamily(family); + return *this; +} + +FontBuilder FontBuilder::size(int pointSize) { + if (pointSize <= 0) + pointSize = 1; + m_font.setPointSize(pointSize); + m_font.setVariableAxis("opsz", static_cast(pointSize)); + return *this; +} + +FontBuilder FontBuilder::weight(QFont::Weight weight) { + m_font.setWeight(weight); + m_font.setVariableAxis("wght", weight); + return *this; +} + +FontBuilder FontBuilder::italic(bool on) { + m_font.setItalic(on); + return *this; +} + +FontBuilder FontBuilder::stretch(int stretch) { + m_font.setStretch(stretch); + return *this; +} + +FontBuilder FontBuilder::letterSpacing(qreal spacing, bool absolute) { + m_font.setLetterSpacing(absolute ? QFont::AbsoluteSpacing : QFont::PercentageSpacing, spacing); + return *this; +} + +FontBuilder FontBuilder::capitalisation(QFont::Capitalization cap) { + m_font.setCapitalization(cap); + return *this; +} + +FontBuilder FontBuilder::vaxis(const QString& tag, float value) { + if (auto t = QFont::Tag::fromString(tag)) + m_font.setVariableAxis(*t, value); + else + qCWarning(lcFontBuilder) << "Unable to convert tag" << tag << "to QFont::Tag"; + return *this; +} + +FontBuilder FontBuilder::vaxes(QVariantMap axes) { + for (auto it = axes.constBegin(); it != axes.constEnd(); ++it) { + if (it.value().canConvert()) { + if (auto tag = QFont::Tag::fromString(it.key())) + m_font.setVariableAxis(*tag, it.value().toFloat()); + else + qCWarning(lcFontBuilder) << "Unable to convert tag" << it.key() << "to QFont::Tag"; + } else { + qCWarning(lcFontBuilder) << "Unable to convert value" << it.value() << "to float"; + } + } + return *this; +} + +QFont FontBuilder::build() const { + return m_font; +} + +FontBuilder FontBuilder::fill(float value) { + return vaxis("FILL", value); +} + +FontBuilder FontBuilder::grade(float value) { + return vaxis("GRAD", value); +} + +FontBuilder FontBuilder::width(float value) { + return vaxis("wdth", value); +} + +FontBuilder FontBuilder::scale(qreal factor) { + return size(static_cast(m_font.pointSize() * factor)); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/fontbuilder.hpp b/plugin/src/Caelestia/Config/fontbuilder.hpp new file mode 100644 index 00000000..b159d08b --- /dev/null +++ b/plugin/src/Caelestia/Config/fontbuilder.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +namespace caelestia::config { + +class FontBuilder { + Q_GADGET + QML_ANONYMOUS + +public: + FontBuilder() = default; + explicit FontBuilder(QFont font); + + [[nodiscard]] Q_INVOKABLE FontBuilder family(const QString& family); + [[nodiscard]] Q_INVOKABLE FontBuilder size(int pointSize); + [[nodiscard]] Q_INVOKABLE FontBuilder weight(QFont::Weight weight); + [[nodiscard]] Q_INVOKABLE FontBuilder italic(bool on = true); + [[nodiscard]] Q_INVOKABLE FontBuilder stretch(int stretch); + [[nodiscard]] Q_INVOKABLE FontBuilder letterSpacing(qreal spacing, bool absolute = true); + [[nodiscard]] Q_INVOKABLE FontBuilder capitalisation(QFont::Capitalization cap); + [[nodiscard]] Q_INVOKABLE FontBuilder vaxis(const QString& tag, float value); + [[nodiscard]] Q_INVOKABLE FontBuilder vaxes(QVariantMap axes); + [[nodiscard]] Q_INVOKABLE QFont build() const; + + // Common vaxes + [[nodiscard]] Q_INVOKABLE FontBuilder fill(float value); + [[nodiscard]] Q_INVOKABLE FontBuilder grade(float value); + [[nodiscard]] Q_INVOKABLE FontBuilder width(float value); + + // Operations + [[nodiscard]] Q_INVOKABLE FontBuilder scale(qreal factor); + +private: + QFont m_font; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/launcherconfig.hpp b/plugin/src/Caelestia/Config/launcherconfig.hpp index 049e213a..49764599 100644 --- a/plugin/src/Caelestia/Config/launcherconfig.hpp +++ b/plugin/src/Caelestia/Config/launcherconfig.hpp @@ -122,7 +122,7 @@ class LauncherConfig : public ConfigObject { { u"name"_s, u"Settings"_s }, { u"icon"_s, u"settings"_s }, { u"description"_s, u"Configure the shell"_s }, - { u"command"_s, QStringList{ u"caelestia"_s, u"shell"_s, u"controlCenter"_s, u"open"_s } }, + { u"command"_s, QStringList{ u"caelestia"_s, u"shell"_s, u"nexus"_s, u"open"_s } }, }), }) diff --git a/plugin/src/Caelestia/Config/lockconfig.hpp b/plugin/src/Caelestia/Config/lockconfig.hpp index 0d8aa6ba..f44d398d 100644 --- a/plugin/src/Caelestia/Config/lockconfig.hpp +++ b/plugin/src/Caelestia/Config/lockconfig.hpp @@ -8,7 +8,7 @@ class LockConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(bool, recolourLogo, false) + CONFIG_PROPERTY(bool, recolourLogo, true) CONFIG_GLOBAL_PROPERTY(bool, enableFprint, true) CONFIG_GLOBAL_PROPERTY(int, maxFprintTries, 3) CONFIG_PROPERTY(bool, hideNotifs, false) diff --git a/plugin/src/Caelestia/Config/nexusconfig.hpp b/plugin/src/Caelestia/Config/nexusconfig.hpp new file mode 100644 index 00000000..2389acb3 --- /dev/null +++ b/plugin/src/Caelestia/Config/nexusconfig.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +class NexusConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, wallpapersPerRow, 4) + CONFIG_GLOBAL_PROPERTY(int, networkRescanInterval, 15000) + +public: + explicit NexusConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/serviceconfig.hpp b/plugin/src/Caelestia/Config/serviceconfig.hpp index b97c69c1..ffdccda6 100644 --- a/plugin/src/Caelestia/Config/serviceconfig.hpp +++ b/plugin/src/Caelestia/Config/serviceconfig.hpp @@ -24,7 +24,7 @@ class ServiceConfig : public ConfigObject { CONFIG_GLOBAL_PROPERTY( bool, useTwelveHourClock, QLocale().timeFormat(QLocale::ShortFormat).toLower().contains(u"a"_s)) CONFIG_GLOBAL_PROPERTY(QString, gpuType) - CONFIG_GLOBAL_PROPERTY(int, visualiserBars, 45) + CONFIG_GLOBAL_PROPERTY(int, visualiserBars, 60) CONFIG_GLOBAL_PROPERTY(qreal, audioIncrement, 0.1) CONFIG_GLOBAL_PROPERTY(qreal, brightnessIncrement, 0.1) CONFIG_GLOBAL_PROPERTY(qreal, maxVolume, 1.0) @@ -32,7 +32,6 @@ class ServiceConfig : public ConfigObject { CONFIG_GLOBAL_PROPERTY(QString, defaultPlayer, u"Spotify"_s) CONFIG_GLOBAL_PROPERTY(QVariantList, playerAliases, { vmap({ { u"from"_s, u"com.github.th_ch.youtube_music"_s }, { u"to"_s, u"YT Music"_s } }) }) - CONFIG_GLOBAL_PROPERTY(bool, showLyrics, false) CONFIG_GLOBAL_PROPERTY(QString, lyricsBackend, u"Auto"_s) public: diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 28bac1f1..83218c65 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -2,6 +2,7 @@ #include "rootconfig.hpp" +#include #include #include @@ -46,10 +47,14 @@ class RoundingTokens : public ConfigObject { QML_ANONYMOUS CONFIG_PROPERTY(int, extraSmall, 4) - CONFIG_PROPERTY(int, small, 12) - CONFIG_PROPERTY(int, normal, 17) - CONFIG_PROPERTY(int, large, 25) - CONFIG_PROPERTY(int, full, 1000) + CONFIG_PROPERTY(int, small, 8) + CONFIG_PROPERTY(int, medium, 12) + CONFIG_PROPERTY(int, large, 16) + CONFIG_PROPERTY(int, largeIncreased, 20) + CONFIG_PROPERTY(int, extraLarge, 28) + CONFIG_PROPERTY(int, extraLargeIncreased, 32) + CONFIG_PROPERTY(int, extraExtraLarge, 48) + CONFIG_PROPERTY(int, full, std::numeric_limits::max()) public: explicit RoundingTokens(QObject* parent = nullptr) @@ -60,11 +65,14 @@ class SpacingTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(int, small, 7) - CONFIG_PROPERTY(int, smaller, 10) - CONFIG_PROPERTY(int, normal, 12) - CONFIG_PROPERTY(int, larger, 15) - CONFIG_PROPERTY(int, large, 20) + CONFIG_PROPERTY(int, extraSmall, 4) + CONFIG_PROPERTY(int, small, 8) + CONFIG_PROPERTY(int, medium, 12) + CONFIG_PROPERTY(int, large, 16) + CONFIG_PROPERTY(int, largeIncreased, 20) + CONFIG_PROPERTY(int, extraLarge, 28) + CONFIG_PROPERTY(int, extraLargeIncreased, 32) + CONFIG_PROPERTY(int, extraExtraLarge, 48) public: explicit SpacingTokens(QObject* parent = nullptr) @@ -75,11 +83,14 @@ class PaddingTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(int, small, 5) - CONFIG_PROPERTY(int, smaller, 7) - CONFIG_PROPERTY(int, normal, 10) - CONFIG_PROPERTY(int, larger, 12) - CONFIG_PROPERTY(int, large, 15) + CONFIG_PROPERTY(int, extraSmall, 4) + CONFIG_PROPERTY(int, small, 8) + CONFIG_PROPERTY(int, medium, 12) + CONFIG_PROPERTY(int, large, 16) + CONFIG_PROPERTY(int, largeIncreased, 20) + CONFIG_PROPERTY(int, extraLarge, 28) + CONFIG_PROPERTY(int, extraLargeIncreased, 32) + CONFIG_PROPERTY(int, extraExtraLarge, 48) public: explicit PaddingTokens(QObject* parent = nullptr) @@ -166,17 +177,28 @@ class DashboardTokens : public ConfigObject { CONFIG_PROPERTY(int, tabIndicatorHeight, 3) CONFIG_PROPERTY(int, tabIndicatorSpacing, 5) - CONFIG_PROPERTY(int, infoWidth, 200) - CONFIG_PROPERTY(int, infoIconSize, 25) + CONFIG_PROPERTY(int, userWidth, 340) + CONFIG_PROPERTY(int, logoSize, 30) + CONFIG_PROPERTY(int, uptimeSize, 30) CONFIG_PROPERTY(int, dateTimeWidth, 110) CONFIG_PROPERTY(int, mediaWidth, 200) CONFIG_PROPERTY(int, mediaProgressSweep, 180) - CONFIG_PROPERTY(int, mediaProgressThickness, 8) - CONFIG_PROPERTY(int, resourceProgressThickness, 10) - CONFIG_PROPERTY(int, weatherWidth, 250) - CONFIG_PROPERTY(int, mediaCoverArtSize, 150) - CONFIG_PROPERTY(int, mediaVisualiserSize, 80) - CONFIG_PROPERTY(int, resourceSize, 200) + CONFIG_PROPERTY(int, mediaProgressThickness, 6) + CONFIG_PROPERTY(int, resourceProgressThickness, 6) + CONFIG_PROPERTY(int, weatherWidth, 275) + CONFIG_PROPERTY(int, mediaCoverArtSize, 200) + CONFIG_PROPERTY(int, mediaTabWidth, 1000) + CONFIG_PROPERTY(int, mediaTabHeight, 320) + CONFIG_PROPERTY(int, mediaSectionWidth, 300) + CONFIG_PROPERTY(int, perfHeroCardWidth, 400) + CONFIG_PROPERTY(int, perfUsageShapeSize, 100) + CONFIG_PROPERTY(int, perfStorageTextWidth, 160) + CONFIG_PROPERTY(int, perfNetworkCardWidth, 390) + CONFIG_PROPERTY(int, perfNetworkCardHeight, 220) + CONFIG_PROPERTY(int, perfBattWidth, 150) + CONFIG_PROPERTY(int, perfBattWidthSingle, 400) + CONFIG_PROPERTY(int, perfBattHeight, 160) + CONFIG_PROPERTY(int, perfPlaceholderWidth, 700) public: explicit DashboardTokens(QObject* parent = nullptr) @@ -201,8 +223,8 @@ class NotifsTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(int, width, 400) - CONFIG_GLOBAL_PROPERTY(int, image, 41) + CONFIG_PROPERTY(int, width, 430) + CONFIG_GLOBAL_PROPERTY(int, image, 42) CONFIG_PROPERTY(int, badge, 20) public: @@ -263,6 +285,14 @@ class LockTokens : public ConfigObject { CONFIG_PROPERTY(qreal, heightMult, 0.7) CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) CONFIG_PROPERTY(int, centerWidth, 600) + CONFIG_PROPERTY(int, showWeatherDetailsHeight, 500) + CONFIG_PROPERTY(int, showForecastHeight, 975) + CONFIG_PROPERTY(int, forecastItemWidth, 51) + CONFIG_PROPERTY(int, largeLogoWidth, 320) + CONFIG_PROPERTY(int, largeFontWidth, 400) + CONFIG_PROPERTY(int, fetch4LinesHeight, 600) + CONFIG_PROPERTY(int, fetch3LinesHeight, 500) + CONFIG_PROPERTY(int, showColourBoxRowHeight, 570) public: explicit LockTokens(QObject* parent = nullptr) @@ -281,15 +311,18 @@ public: : ConfigObject(parent) {} }; -class ControlCenterTokens : public ConfigObject { +class NexusTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS CONFIG_PROPERTY(qreal, heightMult, 0.7) CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) + CONFIG_PROPERTY(int, minWidth, 800) + CONFIG_PROPERTY(int, minHeight, 500) + CONFIG_PROPERTY(int, maxNavWidth, 600) public: - explicit ControlCenterTokens(QObject* parent = nullptr) + explicit NexusTokens(QObject* parent = nullptr) : ConfigObject(parent) {} }; @@ -307,7 +340,7 @@ class SizeTokens : public ConfigObject { CONFIG_SUBOBJECT(UtilitiesTokens, utilities) CONFIG_SUBOBJECT(LockTokens, lock) CONFIG_SUBOBJECT(WInfoTokens, winfo) - CONFIG_SUBOBJECT(ControlCenterTokens, controlCenter) + CONFIG_SUBOBJECT(NexusTokens, nexus) public: explicit SizeTokens(QObject* parent = nullptr) @@ -322,7 +355,7 @@ public: , m_utilities(new UtilitiesTokens(this)) , m_lock(new LockTokens(this)) , m_winfo(new WInfoTokens(this)) - , m_controlCenter(new ControlCenterTokens(this)) {} + , m_nexus(new NexusTokens(this)) {} }; class TokenConfig : public RootConfig { diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp index 16f863b8..d7225981 100644 --- a/plugin/src/Caelestia/Config/tokensattached.cpp +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -2,6 +2,7 @@ #include "anim.hpp" #include "appearanceconfig.hpp" #include "config.hpp" +#include "font.hpp" #include "monitorconfigmanager.hpp" #include "tokens.hpp" @@ -23,8 +24,10 @@ const AppearanceConfig* resolveAppearance(GlobalConfig* config, bool complete, c Tokens::Tokens(QObject* parent) : QQuickAttachedPropertyPropagator(parent) + , m_font(new FontTokens(this)) , m_anim(new AnimTokens(this)) { bindAnim(); + bindFont(); initialize(); } @@ -52,6 +55,7 @@ void Tokens::inheritScreen(const QString& screen) { m_tokens = MonitorConfigManager::instance()->tokensForScreen(m_screen); } + bindFont(); propagateScreen(); emit sourceChanged(); } @@ -78,6 +82,11 @@ void Tokens::bindAnim() { m_anim->bindCurves(TokenConfig::instance()->appearance()->curves()); } +void Tokens::bindFont() { + auto* appearance = m_config ? m_config->appearance() : GlobalConfig::instance()->appearance(); + m_font->bindFont(appearance->font()); +} + #define TOKENS_ATTACHED_GETTER(Type, name) \ const Type* Tokens::name() const { \ auto* a = resolveAppearance(m_config, m_complete, #name, parent()); \ @@ -87,7 +96,6 @@ void Tokens::bindAnim() { TOKENS_ATTACHED_GETTER(AppearanceRounding, rounding) TOKENS_ATTACHED_GETTER(AppearanceSpacing, spacing) TOKENS_ATTACHED_GETTER(AppearancePadding, padding) -TOKENS_ATTACHED_GETTER(AppearanceFont, font) #undef TOKENS_ATTACHED_GETTER @@ -103,6 +111,10 @@ const SizeTokens* Tokens::sizes() const { return TokenConfig::instance()->sizes(); } +const FontTokens* Tokens::font() const { + return m_font; +} + const AnimTokens* Tokens::anim() const { return m_anim; } diff --git a/plugin/src/Caelestia/Config/tokensattached.hpp b/plugin/src/Caelestia/Config/tokensattached.hpp index 810b0c0d..623b2adc 100644 --- a/plugin/src/Caelestia/Config/tokensattached.hpp +++ b/plugin/src/Caelestia/Config/tokensattached.hpp @@ -7,11 +7,11 @@ namespace caelestia::config { class AnimTokens; -class AppearanceFont; class AppearancePadding; class AppearanceRounding; class AppearanceSpacing; class AppearanceTransparency; +class FontTokens; class GlobalConfig; class SizeTokens; class TokenConfig; @@ -24,15 +24,16 @@ class Tokens : public QQuickAttachedPropertyPropagator, public QQmlParserStatus QML_ATTACHED(Tokens) Q_MOC_INCLUDE("anim.hpp") Q_MOC_INCLUDE("appearanceconfig.hpp") + Q_MOC_INCLUDE("font.hpp") Q_MOC_INCLUDE("tokens.hpp") Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::AppearanceFont* font READ font NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceTransparency* transparency READ transparency NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::SizeTokens* sizes READ sizes NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::FontTokens* font READ font NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AnimTokens* anim READ anim NOTIFY sourceChanged) public: @@ -44,10 +45,10 @@ public: [[nodiscard]] const AppearanceRounding* rounding() const; [[nodiscard]] const AppearanceSpacing* spacing() const; [[nodiscard]] const AppearancePadding* padding() const; - [[nodiscard]] const AppearanceFont* font() const; [[nodiscard]] const AppearanceTransparency* transparency() const; [[nodiscard]] const SizeTokens* sizes() const; + [[nodiscard]] const FontTokens* font() const; [[nodiscard]] const AnimTokens* anim() const; [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); @@ -67,11 +68,13 @@ private: void propagateScreen(); void bindAnim(); + void bindFont(); bool m_complete = false; QString m_screen; GlobalConfig* m_config = nullptr; TokenConfig* m_tokens = nullptr; + FontTokens* m_font = nullptr; AnimTokens* m_anim = nullptr; }; diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt index f4bbc5fd..abcaf86b 100644 --- a/plugin/src/Caelestia/Internal/CMakeLists.txt +++ b/plugin/src/Caelestia/Internal/CMakeLists.txt @@ -1,9 +1,9 @@ qml_module(caelestia-internal URI Caelestia.Internal SOURCES - arcgauge.cpp circularbuffer.cpp circularindicatormanager.cpp + linearindicatormanager.cpp hyprdevices.cpp hyprextras.cpp logindmanager.cpp diff --git a/plugin/src/Caelestia/Internal/arcgauge.cpp b/plugin/src/Caelestia/Internal/arcgauge.cpp deleted file mode 100644 index d534f5f7..00000000 --- a/plugin/src/Caelestia/Internal/arcgauge.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "arcgauge.hpp" - -#include -#include -#include - -namespace caelestia::internal { - -ArcGauge::ArcGauge(QQuickItem* parent) - : QQuickPaintedItem(parent) { - setAntialiasing(true); -} - -void ArcGauge::paint(QPainter* painter) { - const qreal w = width(); - const qreal h = height(); - const qreal side = qMin(w, h); - const qreal radius = (side - m_lineWidth - 2.0) / 2.0; - const qreal cx = w / 2.0; - const qreal cy = h / 2.0; - - const QRectF arcRect(cx - radius, cy - radius, radius * 2.0, radius * 2.0); - - // Convert from Canvas convention (CW radians from 3 o'clock) to QPainter (CCW 1/16th degrees) - const int startAngle16 = qRound(-(m_startAngle * 180.0 / M_PI) * 16.0); - const int sweepAngle16 = qRound(-(m_sweepAngle * 180.0 / M_PI) * 16.0); - - painter->setRenderHint(QPainter::Antialiasing, true); - - // Draw track arc - QPen trackPen(m_trackColor, m_lineWidth); - trackPen.setCapStyle(Qt::RoundCap); - painter->setPen(trackPen); - painter->setBrush(Qt::NoBrush); - painter->drawArc(arcRect, startAngle16, sweepAngle16); - - // Draw value arc - if (m_percentage > 0.0) { - const int valueSweep16 = qRound(static_cast(sweepAngle16) * m_percentage); - QPen valuePen(m_accentColor, m_lineWidth); - valuePen.setCapStyle(Qt::RoundCap); - painter->setPen(valuePen); - painter->drawArc(arcRect, startAngle16, valueSweep16); - } -} - -qreal ArcGauge::percentage() const { - return m_percentage; -} - -void ArcGauge::setPercentage(qreal percentage) { - if (qFuzzyCompare(m_percentage, percentage)) - return; - m_percentage = percentage; - emit percentageChanged(); - update(); -} - -QColor ArcGauge::accentColor() const { - return m_accentColor; -} - -void ArcGauge::setAccentColor(const QColor& color) { - if (m_accentColor == color) - return; - m_accentColor = color; - emit accentColorChanged(); - update(); -} - -QColor ArcGauge::trackColor() const { - return m_trackColor; -} - -void ArcGauge::setTrackColor(const QColor& color) { - if (m_trackColor == color) - return; - m_trackColor = color; - emit trackColorChanged(); - update(); -} - -qreal ArcGauge::startAngle() const { - return m_startAngle; -} - -void ArcGauge::setStartAngle(qreal angle) { - if (qFuzzyCompare(m_startAngle, angle)) - return; - m_startAngle = angle; - emit startAngleChanged(); - update(); -} - -qreal ArcGauge::sweepAngle() const { - return m_sweepAngle; -} - -void ArcGauge::setSweepAngle(qreal angle) { - if (qFuzzyCompare(m_sweepAngle, angle)) - return; - m_sweepAngle = angle; - emit sweepAngleChanged(); - update(); -} - -qreal ArcGauge::lineWidth() const { - return m_lineWidth; -} - -void ArcGauge::setLineWidth(qreal width) { - if (qFuzzyCompare(m_lineWidth, width)) - return; - m_lineWidth = width; - emit lineWidthChanged(); - update(); -} - -} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/arcgauge.hpp b/plugin/src/Caelestia/Internal/arcgauge.hpp deleted file mode 100644 index 4ccb1fd0..00000000 --- a/plugin/src/Caelestia/Internal/arcgauge.hpp +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace caelestia::internal { - -class ArcGauge : public QQuickPaintedItem { - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(qreal percentage READ percentage WRITE setPercentage NOTIFY percentageChanged) - Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged) - Q_PROPERTY(QColor trackColor READ trackColor WRITE setTrackColor NOTIFY trackColorChanged) - Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged) - Q_PROPERTY(qreal sweepAngle READ sweepAngle WRITE setSweepAngle NOTIFY sweepAngleChanged) - Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) - -public: - explicit ArcGauge(QQuickItem* parent = nullptr); - - void paint(QPainter* painter) override; - - [[nodiscard]] qreal percentage() const; - void setPercentage(qreal percentage); - - [[nodiscard]] QColor accentColor() const; - void setAccentColor(const QColor& color); - - [[nodiscard]] QColor trackColor() const; - void setTrackColor(const QColor& color); - - [[nodiscard]] qreal startAngle() const; - void setStartAngle(qreal angle); - - [[nodiscard]] qreal sweepAngle() const; - void setSweepAngle(qreal angle); - - [[nodiscard]] qreal lineWidth() const; - void setLineWidth(qreal width); - -signals: - void percentageChanged(); - void accentColorChanged(); - void trackColorChanged(); - void startAngleChanged(); - void sweepAngleChanged(); - void lineWidthChanged(); - -private: - qreal m_percentage = 0.0; - QColor m_accentColor; - QColor m_trackColor; - qreal m_startAngle = 0.75 * M_PI; - qreal m_sweepAngle = 1.5 * M_PI; - qreal m_lineWidth = 10.0; -}; - -} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/linearindicatormanager.cpp b/plugin/src/Caelestia/Internal/linearindicatormanager.cpp new file mode 100644 index 00000000..597a593d --- /dev/null +++ b/plugin/src/Caelestia/Internal/linearindicatormanager.cpp @@ -0,0 +1,119 @@ +#include "linearindicatormanager.hpp" + +#include + +namespace { + +// See +// https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/progressindicator/LinearIndeterminateDisjointAnimatorDelegate.java#L44-L46 +constexpr int TOTAL_DURATION_IN_MS = 1800; +constexpr std::array DURATION_TO_MOVE_SEGMENT_ENDS = { 533, 567, 850, 750 }; +constexpr std::array DELAY_TO_MOVE_SEGMENT_ENDS = { 1267, 1000, 333, 0 }; + +QEasingCurve curve(const QPointF& c1, const QPointF& c2) { + QEasingCurve curve(QEasingCurve::BezierSpline); + curve.addCubicBezierSegment(c1, c2, { 1.0, 1.0 }); + return curve; +} + +qreal getFractionInRange(qreal playtime, int start, int duration) { + const auto fraction = static_cast(playtime - start) / duration; + return std::clamp(fraction, 0.0, 1.0); +} + +} // namespace + +namespace caelestia::controls { + +LinearIndicatorSegment::LinearIndicatorSegment(int gap, QObject* parent) + : QObject(parent) + , m_startFraction(0) + , m_endFraction(0) + , m_gapSize(gap) {} + +qreal LinearIndicatorSegment::startFraction() const { + return m_startFraction; +} + +qreal LinearIndicatorSegment::endFraction() const { + return m_endFraction; +} + +int LinearIndicatorSegment::gapSize() const { + return m_gapSize; +} + +LinearIndicatorManager::LinearIndicatorManager(QObject* parent) + : QObject(parent) + , m_interpolators({ + curve({ 0.2, 0.0 }, { 0.8, 1.0 }), + curve({ 0.4, 0.0 }, { 1.0, 1.0 }), + curve({ 0.0, 0.0 }, { 0.65, 1.0 }), + curve({ 0.1, 0.0 }, { 0.45, 1.0 }), + }) + , m_progress(0) + , m_completeEndProgress(0) + , m_gap(4) + , m_activeIndicators({ + new LinearIndicatorSegment(m_gap, this), + new LinearIndicatorSegment(m_gap, this), + }) { + for (auto el : m_activeIndicators) + QObject::connect(this, &LinearIndicatorManager::updated, el, &LinearIndicatorSegment::updated); +} + +QList LinearIndicatorManager::activeIndicators() const { + return { m_activeIndicators.cbegin(), m_activeIndicators.cend() }; +} + +qreal LinearIndicatorManager::progress() const { + return m_progress; +} + +qreal LinearIndicatorManager::completeEndProgress() const { + return m_completeEndProgress; +} + +int LinearIndicatorManager::gap() const { + return m_gap; +} + +void LinearIndicatorManager::setGap(int gap) { + m_gap = gap; + for (auto el : m_activeIndicators) + el->m_gapSize = m_gap; + update(m_progress); +} + +int LinearIndicatorManager::duration() const { + return TOTAL_DURATION_IN_MS; +} + +int LinearIndicatorManager::completeEndDuration() const { + return TOTAL_DURATION_IN_MS; +} + +void LinearIndicatorManager::update(qreal progress) { + const auto playtime = progress * TOTAL_DURATION_IN_MS; + for (size_t i = 0; i < SEGMENTS; i++) { + const auto di = i * 2; + auto* const indicator = m_activeIndicators[i]; + + auto fraction = getFractionInRange(playtime, DELAY_TO_MOVE_SEGMENT_ENDS[di], DURATION_TO_MOVE_SEGMENT_ENDS[di]); + indicator->m_startFraction = std::clamp(m_interpolators[di].valueForProgress(fraction), 0.0, 1.0); + + fraction = + getFractionInRange(playtime, DELAY_TO_MOVE_SEGMENT_ENDS[di + 1], DURATION_TO_MOVE_SEGMENT_ENDS[di + 1]); + indicator->m_endFraction = std::clamp(m_interpolators[di + 1].valueForProgress(fraction), 0.0, 1.0); + } + + m_progress = progress; + emit updated(); +} + +void LinearIndicatorManager::updateCompleteEndProgress(qreal progress) { + m_completeEndProgress = progress; + update(m_progress); +} + +} // namespace caelestia::controls diff --git a/plugin/src/Caelestia/Internal/linearindicatormanager.hpp b/plugin/src/Caelestia/Internal/linearindicatormanager.hpp new file mode 100644 index 00000000..272e8643 --- /dev/null +++ b/plugin/src/Caelestia/Internal/linearindicatormanager.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace caelestia::controls { + +class LinearIndicatorManager; + +class LinearIndicatorSegment : public QObject { + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("LinearIndicatorSegments can only be retrieved from a " + "LinearIndicatorManager.") + + Q_PROPERTY(qreal startFraction READ startFraction NOTIFY updated FINAL) + Q_PROPERTY(qreal endFraction READ endFraction NOTIFY updated FINAL) + Q_PROPERTY(int gapSize READ gapSize NOTIFY updated FINAL) + +public: + explicit LinearIndicatorSegment(int gap, QObject* parent = nullptr); + + qreal startFraction() const; + qreal endFraction() const; + int gapSize() const; + +signals: + void updated(); + +private: + qreal m_startFraction; + qreal m_endFraction; + int m_gapSize; + + friend LinearIndicatorManager; +}; + +class LinearIndicatorManager : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY( + QList activeIndicators READ activeIndicators CONSTANT FINAL) + + Q_PROPERTY(qreal progress READ progress WRITE update NOTIFY updated FINAL) + Q_PROPERTY(qreal completeEndProgress READ completeEndProgress WRITE updateCompleteEndProgress NOTIFY updated FINAL) + Q_PROPERTY(int gap READ gap WRITE setGap NOTIFY updated FINAL) + + Q_PROPERTY(qreal duration READ duration CONSTANT FINAL) + Q_PROPERTY(qreal completeEndDuration READ completeEndDuration CONSTANT FINAL) + +public: + explicit LinearIndicatorManager(QObject* parent = nullptr); + + QList activeIndicators() const; + + qreal progress() const; + qreal completeEndProgress() const; + + int gap() const; + void setGap(int gap); + + int duration() const; + int completeEndDuration() const; + + void update(qreal progress); + void updateCompleteEndProgress(qreal progress); + +signals: + void updated(); + +private: + static constexpr int SEGMENTS = 2; + + std::array m_interpolators; + qreal m_progress; + qreal m_completeEndProgress; + int m_gap; + + std::array m_activeIndicators; +}; + +} // namespace caelestia::controls diff --git a/plugin/src/Caelestia/Services/CMakeLists.txt b/plugin/src/Caelestia/Services/CMakeLists.txt index 8ce868b5..5c85ae5f 100644 --- a/plugin/src/Caelestia/Services/CMakeLists.txt +++ b/plugin/src/Caelestia/Services/CMakeLists.txt @@ -1,14 +1,27 @@ qml_module(caelestia-services URI Caelestia.Services SOURCES - service.hpp service.cpp - serviceref.hpp serviceref.cpp - beattracker.hpp beattracker.cpp - audiocollector.hpp audiocollector.cpp - audioprovider.hpp audioprovider.cpp - cavaprovider.hpp cavaprovider.cpp + service.cpp + serviceref.cpp + beattracker.cpp + audiocollector.cpp + audioprovider.cpp + cavaprovider.cpp + tickingservice.cpp + sensorslib.cpp + usagefmt.cpp + cpu.cpp + gpu.cpp + memory.cpp + diskinfo.cpp + storage.cpp + lyriccandidate.cpp + lyrics.cpp LIBRARIES PkgConfig::Pipewire PkgConfig::Aubio PkgConfig::Cava + Sensors::Sensors + caelestia-config + caelestia-internal ) diff --git a/plugin/src/Caelestia/Services/cpu.cpp b/plugin/src/Caelestia/Services/cpu.cpp new file mode 100644 index 00000000..2872508a --- /dev/null +++ b/plugin/src/Caelestia/Services/cpu.cpp @@ -0,0 +1,117 @@ +#include "cpu.hpp" + +#include "sensorslib.hpp" + +#include +#include +#include + +namespace caelestia::services { + +Cpu::Cpu(QObject* parent) + : TickingService(parent) { + readNameOnce(); +} + +QString Cpu::name() const { + return m_name; +} + +qreal Cpu::percentage() const { + return m_percentage; +} + +qreal Cpu::temperature() const { + return m_temperature; +} + +void Cpu::tick() { + if (!m_nameLoaded) { + readNameOnce(); + } + refreshPercentage(); + refreshTemperature(); +} + +void Cpu::readNameOnce() { + QFile f(QStringLiteral("/proc/cpuinfo")); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return; + } + const QByteArray data = f.readAll(); + f.close(); + + static const QRegularExpression re(QStringLiteral("model name\\s*:\\s*(.+)")); + const auto match = re.match(QString::fromLatin1(data)); + if (!match.hasMatch()) { + return; + } + + const QString cleaned = cleanName(match.captured(1)); + m_nameLoaded = true; + if (cleaned == m_name) { + return; + } + m_name = cleaned; + Q_EMIT nameChanged(); +} + +void Cpu::refreshPercentage() { + QFile f(QStringLiteral("/proc/stat")); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return; + } + const QByteArray data = f.readAll(); + f.close(); + + static const QRegularExpression re( + QStringLiteral("^cpu\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)")); + const auto match = re.match(QString::fromLatin1(data)); + if (!match.hasMatch()) { + return; + } + + quint64 total = 0; + quint64 idle = 0; + for (int i = 1; i <= 7; ++i) { + const quint64 v = match.captured(i).toULongLong(); + total += v; + if (i == 4 || i == 5) { + idle += v; + } + } + + const quint64 totalDiff = total > m_lastTotal ? total - m_lastTotal : 0; + const quint64 idleDiff = idle > m_lastIdle ? idle - m_lastIdle : 0; + const qreal newPerc = totalDiff > 0 ? 1.0 - static_cast(idleDiff) / static_cast(totalDiff) : 0.0; + + m_lastTotal = total; + m_lastIdle = idle; + + if (std::abs(newPerc - m_percentage) > 0.0001) { + m_percentage = newPerc; + Q_EMIT percentageChanged(); + } +} + +void Cpu::refreshTemperature() { + const auto t = sensorslib::cpuPackageTemp(); + const qreal newTemp = t.value_or(0.0); + if (std::abs(newTemp - m_temperature) > 0.05) { + m_temperature = newTemp; + Q_EMIT temperatureChanged(); + } +} + +QString Cpu::cleanName(QString s) { + static const QRegularExpression noise( + QStringLiteral("\\(R\\)|\\(TM\\)|CPU|\\d+(?:th|nd|rd|st) Gen |Core |Processor"), + QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression spaces(QStringLiteral("\\s+")); + + s.replace(noise, QString()); + s.replace(spaces, QStringLiteral(" ")); + return s.trimmed(); +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/cpu.hpp b/plugin/src/Caelestia/Services/cpu.hpp new file mode 100644 index 00000000..72db08ff --- /dev/null +++ b/plugin/src/Caelestia/Services/cpu.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "tickingservice.hpp" + +#include + +namespace caelestia::services { + +class Cpu : public TickingService { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged) + Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged) + +public: + explicit Cpu(QObject* parent = nullptr); + + [[nodiscard]] QString name() const; + [[nodiscard]] qreal percentage() const; + [[nodiscard]] qreal temperature() const; + +signals: + void nameChanged(); + void percentageChanged(); + void temperatureChanged(); + +protected: + void tick() override; + +private: + void readNameOnce(); + void refreshPercentage(); + void refreshTemperature(); + + [[nodiscard]] static QString cleanName(QString s); + + QString m_name; + qreal m_percentage = 0.0; + qreal m_temperature = 0.0; + quint64 m_lastIdle = 0; + quint64 m_lastTotal = 0; + bool m_nameLoaded = false; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/diskinfo.cpp b/plugin/src/Caelestia/Services/diskinfo.cpp new file mode 100644 index 00000000..7da2a6cb --- /dev/null +++ b/plugin/src/Caelestia/Services/diskinfo.cpp @@ -0,0 +1,67 @@ +#include "diskinfo.hpp" + +namespace caelestia::services { + +namespace { + +constexpr qreal kKib = 1024.0; + +} // namespace + +DiskInfo::DiskInfo(QString mount, quint64 usedBytes, quint64 totalBytes, bool hasRoot, QObject* parent) + : QObject(parent) + , m_mount(std::move(mount)) + , m_usedBytes(usedBytes) + , m_totalBytes(totalBytes) + , m_hasRoot(hasRoot) {} + +QString DiskInfo::mount() const { + return m_mount; +} + +qreal DiskInfo::used() const { + return static_cast(m_usedBytes) / kKib; +} + +qreal DiskInfo::total() const { + return static_cast(m_totalBytes) / kKib; +} + +qreal DiskInfo::free() const { + const quint64 freeBytes = m_totalBytes > m_usedBytes ? m_totalBytes - m_usedBytes : 0; + return static_cast(freeBytes) / kKib; +} + +qreal DiskInfo::perc() const { + return m_totalBytes > 0 ? static_cast(m_usedBytes) / static_cast(m_totalBytes) : 0.0; +} + +bool DiskInfo::hasRoot() const { + return m_hasRoot; +} + +void DiskInfo::update(quint64 usedBytes, quint64 totalBytes, bool hasRoot) { + const bool usedDiff = usedBytes != m_usedBytes; + const bool totalDiff = totalBytes != m_totalBytes; + const bool rootDiff = hasRoot != m_hasRoot; + + m_usedBytes = usedBytes; + m_totalBytes = totalBytes; + m_hasRoot = hasRoot; + + if (usedDiff) { + Q_EMIT usedChanged(); + } + if (totalDiff) { + Q_EMIT totalChanged(); + } + if (usedDiff || totalDiff) { + Q_EMIT freeChanged(); + Q_EMIT percChanged(); + } + if (rootDiff) { + Q_EMIT hasRootChanged(); + } +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/diskinfo.hpp b/plugin/src/Caelestia/Services/diskinfo.hpp new file mode 100644 index 00000000..331d87b6 --- /dev/null +++ b/plugin/src/Caelestia/Services/diskinfo.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +namespace caelestia::services { + +class DiskInfo : public QObject { + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("DiskInfo is created by DiskUsage") + + Q_PROPERTY(QString mount READ mount CONSTANT) + Q_PROPERTY(qreal used READ used NOTIFY usedChanged) + Q_PROPERTY(qreal total READ total NOTIFY totalChanged) + Q_PROPERTY(qreal free READ free NOTIFY freeChanged) + Q_PROPERTY(qreal perc READ perc NOTIFY percChanged) + Q_PROPERTY(bool hasRoot READ hasRoot NOTIFY hasRootChanged) + +public: + DiskInfo(QString mount, quint64 usedBytes, quint64 totalBytes, bool hasRoot, QObject* parent = nullptr); + + [[nodiscard]] QString mount() const; + [[nodiscard]] qreal used() const; // KiB + [[nodiscard]] qreal total() const; // KiB + [[nodiscard]] qreal free() const; // KiB + [[nodiscard]] qreal perc() const; + [[nodiscard]] bool hasRoot() const; + + void update(quint64 usedBytes, quint64 totalBytes, bool hasRoot); + +signals: + void usedChanged(); + void totalChanged(); + void freeChanged(); + void percChanged(); + void hasRootChanged(); + +private: + QString m_mount; + quint64 m_usedBytes; + quint64 m_totalBytes; + bool m_hasRoot; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/gpu.cpp b/plugin/src/Caelestia/Services/gpu.cpp new file mode 100644 index 00000000..c9b7fa3e --- /dev/null +++ b/plugin/src/Caelestia/Services/gpu.cpp @@ -0,0 +1,256 @@ +#include "gpu.hpp" + +#include "../Config/config.hpp" +#include "../Config/serviceconfig.hpp" +#include "sensorslib.hpp" + +#include +#include +#include +#include + +namespace caelestia::services { + +namespace { + +constexpr const char* kTypeDetectScript = + "if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then echo NVIDIA;" + " elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC;" + " else echo NONE; fi"; + +constexpr const char* kNameDetectScript = "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null" + " || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1" + " || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1"; + +} // namespace + +Gpu::Gpu(QObject* parent) + : TickingService(parent) { + auto* svc = caelestia::config::GlobalConfig::instance()->services(); + m_userType = parseType(svc->gpuType()); + QObject::connect(svc, &caelestia::config::ServiceConfig::gpuTypeChanged, this, [this, svc] { + setUserType(parseType(svc->gpuType())); + }); + + // Detection must run before any ServiceRef appears: callers may gate the ref on + // `type !== Gpu.None`, which would otherwise deadlock the detection. + if (m_userType == Auto) { + detectTypeOnce(); + } + detectNameOnce(); +} + +Gpu::Type Gpu::type() const { + return m_userType == Auto ? m_autoType : m_userType; +} + +Gpu::Type Gpu::userType() const { + return m_userType; +} + +Gpu::Type Gpu::autoType() const { + return m_autoType; +} + +QString Gpu::name() const { + return m_name; +} + +qreal Gpu::percentage() const { + return m_percentage; +} + +qreal Gpu::temperature() const { + return m_temperature; +} + +void Gpu::setUserType(Type value) { + if (value == m_userType) { + return; + } + const Type prevDerived = type(); + m_userType = value; + Q_EMIT userTypeChanged(); + if (type() != prevDerived) { + Q_EMIT typeChanged(); + } +} + +void Gpu::setAutoType(Type value) { + if (value == m_autoType) { + return; + } + const Type prevDerived = type(); + m_autoType = value; + Q_EMIT autoTypeChanged(); + if (type() != prevDerived) { + Q_EMIT typeChanged(); + } +} + +void Gpu::setName(QString value) { + if (value == m_name) { + return; + } + m_name = std::move(value); + Q_EMIT nameChanged(); +} + +void Gpu::tick() { + const Type t = type(); + if (t == Generic) { + readGenericUsage(); + readGpuTemperature(); + } else if (t == Nvidia) { + startNvidiaUsage(); + } else { + if (std::abs(m_percentage) > 0.0001) { + m_percentage = 0.0; + Q_EMIT percentageChanged(); + } + if (std::abs(m_temperature) > 0.05) { + m_temperature = 0.0; + Q_EMIT temperatureChanged(); + } + } +} + +void Gpu::detectTypeOnce() { + if (m_typeProc) { + return; + } + m_typeProc = new QProcess(this); + QObject::connect(m_typeProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) { + const QByteArray out = m_typeProc->readAllStandardOutput().trimmed(); + if (!out.isEmpty()) { + setAutoType(parseType(QString::fromLatin1(out))); + } + m_typeProc->deleteLater(); + m_typeProc = nullptr; + }); + m_typeProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kTypeDetectScript) }); +} + +void Gpu::detectNameOnce() { + if (m_nameProc) { + return; + } + m_nameProc = new QProcess(this); + QObject::connect(m_nameProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) { + const QString output = QString::fromUtf8(m_nameProc->readAllStandardOutput()).trimmed(); + if (!output.isEmpty()) { + const QString lower = output.toLower(); + if (lower.contains(QStringLiteral("nvidia")) || lower.contains(QStringLiteral("geforce")) || + lower.contains(QStringLiteral("rtx")) || lower.contains(QStringLiteral("gtx")) || + lower.contains(QStringLiteral("rx"))) { + setName(cleanName(output)); + } else { + static const QRegularExpression bracketRe(QStringLiteral("\\[([^\\]]+)\\][^\\[]*$")); + const auto bracket = bracketRe.match(output); + if (bracket.hasMatch()) { + setName(cleanName(bracket.captured(1))); + } else { + static const QRegularExpression colonRe(QStringLiteral(":\\s*(.+)")); + const auto colon = colonRe.match(output); + if (colon.hasMatch()) { + setName(cleanName(colon.captured(1))); + } + } + } + } + m_nameProc->deleteLater(); + m_nameProc = nullptr; + }); + m_nameProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript) }); +} + +void Gpu::readGenericUsage() { + const QStringList paths = + QDir(QStringLiteral("/sys/class/drm")) + .entryList(QStringList() << QStringLiteral("card*"), QDir::Dirs | QDir::NoDotAndDotDot); + qreal sum = 0.0; + int count = 0; + for (const QString& card : paths) { + QFile f(QStringLiteral("/sys/class/drm/%1/device/gpu_busy_percent").arg(card)); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + continue; + } + bool ok = false; + const qreal v = f.readAll().trimmed().toDouble(&ok); + f.close(); + if (ok) { + sum += v; + ++count; + } + } + const qreal newPerc = count > 0 ? sum / count / 100.0 : 0.0; + if (std::abs(newPerc - m_percentage) > 0.0001) { + m_percentage = newPerc; + Q_EMIT percentageChanged(); + } +} + +void Gpu::startNvidiaUsage() { + if (m_nvidiaProc) { + return; + } + m_nvidiaProc = new QProcess(this); + QObject::connect(m_nvidiaProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) { + const QByteArray out = m_nvidiaProc->readAllStandardOutput().trimmed(); + m_nvidiaProc->deleteLater(); + m_nvidiaProc = nullptr; + + const QList parts = out.split(','); + if (parts.size() < 2) { + return; + } + bool ok1 = false; + bool ok2 = false; + const qreal usage = parts.at(0).trimmed().toDouble(&ok1) / 100.0; + const qreal temp = parts.at(1).trimmed().toDouble(&ok2); + if (ok1 && std::abs(usage - m_percentage) > 0.0001) { + m_percentage = usage; + Q_EMIT percentageChanged(); + } + if (ok2 && std::abs(temp - m_temperature) > 0.05) { + m_temperature = temp; + Q_EMIT temperatureChanged(); + } + }); + m_nvidiaProc->start(QStringLiteral("nvidia-smi"), { QStringLiteral("--query-gpu=utilization.gpu,temperature.gpu"), + QStringLiteral("--format=csv,noheader,nounits") }); +} + +void Gpu::readGpuTemperature() { + const auto t = sensorslib::gpuPciAverageTemp(); + const qreal newTemp = t.value_or(0.0); + if (std::abs(newTemp - m_temperature) > 0.05) { + m_temperature = newTemp; + Q_EMIT temperatureChanged(); + } +} + +Gpu::Type Gpu::parseType(const QString& s) { + const QString u = s.trimmed().toUpper(); + if (u.isEmpty()) { + return Auto; + } + if (u == QStringLiteral("NVIDIA")) { + return Nvidia; + } + if (u == QStringLiteral("GENERIC")) { + return Generic; + } + return None; +} + +QString Gpu::cleanName(QString s) { + static const QRegularExpression noise( + QStringLiteral("\\(R\\)|\\(TM\\)|Graphics"), QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression spaces(QStringLiteral("\\s+")); + s.replace(noise, QString()); + s.replace(spaces, QStringLiteral(" ")); + return s.trimmed(); +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/gpu.hpp b/plugin/src/Caelestia/Services/gpu.hpp new file mode 100644 index 00000000..7111e554 --- /dev/null +++ b/plugin/src/Caelestia/Services/gpu.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include "tickingservice.hpp" + +#include +#include + +namespace caelestia::services { + +class Gpu : public TickingService { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + enum Type { + Auto, // user override is empty (config "") — defer to detected autoType + None, // no usable GPU + Nvidia, // queried via nvidia-smi + Generic, // queried via /sys/class/drm/card*/device/gpu_busy_percent + }; + Q_ENUM(Type) + +private: + Q_PROPERTY(Type type READ type NOTIFY typeChanged) + Q_PROPERTY(Type userType READ userType NOTIFY userTypeChanged) + Q_PROPERTY(Type autoType READ autoType NOTIFY autoTypeChanged) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged) + Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged) + +public: + explicit Gpu(QObject* parent = nullptr); + + [[nodiscard]] Type type() const; + [[nodiscard]] Type userType() const; + [[nodiscard]] Type autoType() const; + [[nodiscard]] QString name() const; + [[nodiscard]] qreal percentage() const; + [[nodiscard]] qreal temperature() const; + +signals: + void typeChanged(); + void userTypeChanged(); + void autoTypeChanged(); + void nameChanged(); + void percentageChanged(); + void temperatureChanged(); + +protected: + void tick() override; + +private: + void detectTypeOnce(); + void detectNameOnce(); + void readGenericUsage(); + void startNvidiaUsage(); + void readGpuTemperature(); + + void setUserType(Type value); + void setAutoType(Type value); + void setName(QString value); + + [[nodiscard]] static Type parseType(const QString& s); + [[nodiscard]] static QString cleanName(QString s); + + Type m_userType = Auto; + Type m_autoType = None; + QString m_name; + qreal m_percentage = 0.0; + qreal m_temperature = 0.0; + + QProcess* m_typeProc = nullptr; + QProcess* m_nameProc = nullptr; + QProcess* m_nvidiaProc = nullptr; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/lyriccandidate.cpp b/plugin/src/Caelestia/Services/lyriccandidate.cpp new file mode 100644 index 00000000..4d18c123 --- /dev/null +++ b/plugin/src/Caelestia/Services/lyriccandidate.cpp @@ -0,0 +1,65 @@ +#include "lyriccandidate.hpp" + +namespace caelestia::services { + +QString LyricsBackend::toString(Backend b) { + switch (b) { + case LyricsBackend::Auto: + return QStringLiteral("Auto"); + case LyricsBackend::Local: + return QStringLiteral("Local"); + case LyricsBackend::LRCLIB: + return QStringLiteral("LRCLIB"); + case LyricsBackend::NetEase: + return QStringLiteral("NetEase"); + default: + return QStringLiteral("Unknown"); + } +} + +LyricCandidate::LyricCandidate( + LyricsBackend::Backend backend, QString id, QString title, QString artist, QString album, qreal duration) + : m_backend(backend) + , m_id(std::move(id)) + , m_title(std::move(title)) + , m_artist(std::move(artist)) + , m_album(std::move(album)) + , m_duration(duration) {} + +LyricsBackend::Backend LyricCandidate::backend() const { + return m_backend; +} + +QString LyricCandidate::id() const { + return m_id; +} + +QString LyricCandidate::title() const { + return m_title; +} + +QString LyricCandidate::artist() const { + return m_artist; +} + +QString LyricCandidate::album() const { + return m_album; +} + +qreal LyricCandidate::duration() const { + return m_duration; +} + +bool LyricCandidate::isValid() const { + return !m_id.isEmpty(); +} + +bool LyricCandidate::operator==(const LyricCandidate& o) const noexcept { + return m_backend == o.m_backend && m_id == o.m_id; +} + +bool LyricCandidate::operator!=(const LyricCandidate& o) const noexcept { + return !(*this == o); +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/lyriccandidate.hpp b/plugin/src/Caelestia/Services/lyriccandidate.hpp new file mode 100644 index 00000000..7d728306 --- /dev/null +++ b/plugin/src/Caelestia/Services/lyriccandidate.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include + +namespace caelestia::services { + +class LyricsBackend : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + enum Backend { + Auto = 0, + Local, + LRCLIB, + NetEase + }; + Q_ENUM(Backend) + + Q_INVOKABLE QString toString(caelestia::services::LyricsBackend::Backend b); +}; + +class LyricCandidate { + Q_GADGET + QML_VALUE_TYPE(lyricCandidate) + + Q_PROPERTY(caelestia::services::LyricsBackend::Backend backend READ backend) + Q_PROPERTY(QString id READ id) + Q_PROPERTY(QString title READ title) + Q_PROPERTY(QString artist READ artist) + Q_PROPERTY(QString album READ album) + Q_PROPERTY(qreal duration READ duration) + +public: + LyricCandidate() = default; + LyricCandidate(LyricsBackend::Backend backend, QString id, QString title, QString artist, QString album = {}, + qreal duration = 0.0); + + [[nodiscard]] LyricsBackend::Backend backend() const; + [[nodiscard]] QString id() const; + [[nodiscard]] QString title() const; + [[nodiscard]] QString artist() const; + [[nodiscard]] QString album() const; + [[nodiscard]] qreal duration() const; + + [[nodiscard]] bool isValid() const; + bool operator==(const LyricCandidate& o) const noexcept; + bool operator!=(const LyricCandidate& o) const noexcept; + +private: + LyricsBackend::Backend m_backend = LyricsBackend::Auto; + QString m_id; + QString m_title; + QString m_artist; + QString m_album; + qreal m_duration = 0.0; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/lyrics.cpp b/plugin/src/Caelestia/Services/lyrics.cpp new file mode 100644 index 00000000..7490b7ac --- /dev/null +++ b/plugin/src/Caelestia/Services/lyrics.cpp @@ -0,0 +1,1048 @@ +#include "lyrics.hpp" + +#include "../Config/config.hpp" +#include "../Config/serviceconfig.hpp" +#include "../Config/userpaths.hpp" + +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcLyrics, "caelestia.lyrics", QtInfoMsg) + +namespace caelestia::services { + +using Qt::StringLiterals::operator""_s; +using Qt::StringLiterals::operator""_ba; + +namespace { + +constexpr int kLoadDebounceMs = 50; +constexpr qreal kIndexFudge = 0.1; + +[[nodiscard]] const QHash& netEaseHeaders() { + static const QHash h = { + { "User-Agent"_ba, "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0"_ba }, + { "Referer"_ba, "https://music.163.com/"_ba }, + }; + return h; +} + +[[nodiscard]] const QHash& lrclibHeaders() { + static const QHash h = { + { "User-Agent"_ba, "caelestia-shell (https://github.com/caelestia-dots/shell)"_ba }, + }; + return h; +} + +[[nodiscard]] QString joinArtists(const QString& s) { + return s.trimmed(); +} + +[[nodiscard]] QString sanitizeFilenamePart(const QString& s) { + QString out; + out.reserve(s.size()); + for (const QChar c : s) { + if (c == QLatin1Char('/') || c == QLatin1Char('\0')) { + out.append(QLatin1Char('_')); + } else { + out.append(c); + } + } + return out; +} + +[[nodiscard]] bool containsCi(const QString& haystack, const QString& needle) { + return haystack.contains(needle, Qt::CaseInsensitive); +} + +} // namespace + +Lyrics::Lyrics(QObject* parent) + : QObject(parent) + , m_nam(new QNetworkAccessManager(this)) + , m_loadDebounce(new QTimer(this)) { + m_loadDebounce->setSingleShot(true); + m_loadDebounce->setInterval(kLoadDebounceMs); + QObject::connect(m_loadDebounce, &QTimer::timeout, this, &Lyrics::doLoad); + + const auto* cfg = config::GlobalConfig::instance(); + const auto* svcCfg = cfg->services(); + const auto* paths = cfg->paths(); + + m_preferredBackend = backendFromKey(svcCfg->lyricsBackend()); + + QObject::connect( + svcCfg, &config::ServiceConfig::lyricsBackendChanged, this, &Lyrics::onPreferredBackendConfigChanged); + QObject::connect(paths, &config::UserPaths::lyricsDirChanged, this, &Lyrics::onLyricsDirChanged); + + loadLyricsMap(); +} + +QStringList Lyrics::lyrics() const { + return m_lyrics; +} + +LyricsBackend::Backend Lyrics::backend() const { + return m_backend; +} + +LyricsBackend::Backend Lyrics::preferredBackend() const { + return m_preferredBackend; +} + +void Lyrics::setPreferredBackend(LyricsBackend::Backend value) { + if (m_preferredBackend == value) { + return; + } + m_preferredBackend = value; + emit preferredBackendChanged(); + + auto* const svcCfg = config::GlobalConfig::instance()->services(); + const QString key = backendKey(value); + if (svcCfg->lyricsBackend() != key) { + svcCfg->set_lyricsBackend(key); + } + + scheduleLoad(); +} + +QList Lyrics::lyricCandidates() const { + return m_candidates; +} + +LyricCandidate Lyrics::selectedCandidate() const { + return m_selected; +} + +void Lyrics::setSelectedCandidate(const LyricCandidate& value) { + if (m_selected == value) { + return; + } + m_selected = value; + emit selectedCandidateChanged(); + + if (!value.isValid()) { + return; + } + + const auto b = value.backend(); + setBackend(b); + setLoading(true); + + cancelInFlight(); + const int reqId = newRequestId(); + + if (b == LyricsBackend::LRCLIB || b == LyricsBackend::NetEase) { + const QString cached = readCachedLrc(b, value.id()); + if (!cached.isEmpty()) { + const auto lines = parseLrc(cached); + if (!lines.isEmpty()) { + setLines(lines, b); + setLoading(false); + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } + return; + } + } + } + + if (b == LyricsBackend::LRCLIB) { + fetchLrclibById(value.id(), reqId); + } else if (b == LyricsBackend::NetEase) { + fetchNetEaseLyricsById(value.id(), reqId); + } else if (b == LyricsBackend::Local) { + // For local, the id is the file path. Read directly. + QFile f(value.id()); + if (f.open(QIODevice::ReadOnly)) { + const QString text = QString::fromUtf8(f.readAll()); + setLines(parseLrc(text), LyricsBackend::Local); + setLoading(false); + } else { + qCWarning(lcLyrics) << "selectedCandidate: cannot open local file" << value.id(); + setLoading(false); + } + } + + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } +} + +bool Lyrics::loading() const { + return m_loading; +} + +bool Lyrics::hasLyrics() const { + return m_hasLyrics; +} + +qreal Lyrics::offset() const { + return m_offset; +} + +void Lyrics::setOffset(qreal value) { + if (qFuzzyCompare(m_offset, value)) { + return; + } + m_offset = value; + emit offsetChanged(); + + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } +} + +QString Lyrics::trackArtist() const { + return m_artist; +} + +QString Lyrics::trackTitle() const { + return m_title; +} + +int Lyrics::indexForTime(qreal time) const { + if (m_lines.isEmpty()) { + return -1; + } + const qreal target = time - m_offset + kIndexFudge; + qsizetype lo = 0; + qsizetype hi = m_lines.size(); + while (lo < hi) { + const qsizetype mid = lo + (hi - lo) / 2; + if (m_lines.at(mid).time <= target) { + lo = mid + 1; + } else { + hi = mid; + } + } + return static_cast(lo - 1); +} + +void Lyrics::setTrack(const QString& artist, const QString& title, const QString& album, qreal duration) { + const QString a = artist.trimmed(); + const QString t = title.trimmed(); + + if (a == m_artist && t == m_title && album == m_album && qFuzzyCompare(duration + 1.0, m_duration + 1.0)) { + return; + } + + m_artist = a; + m_title = t; + m_album = album; + m_duration = duration; + emit trackChanged(); + + scheduleLoad(); +} + +void Lyrics::clearTrack() { + cancelInFlight(); + m_artist.clear(); + m_title.clear(); + m_album.clear(); + m_duration = 0.0; + emit trackChanged(); + + clearCandidates(); + clearLines(); + setLoading(false); +} + +void Lyrics::refresh() { + scheduleLoad(); +} + +void Lyrics::setBackend(LyricsBackend::Backend value) { + if (m_backend == value) { + return; + } + m_backend = value; + emit backendChanged(); +} + +void Lyrics::setLoading(bool value) { + if (m_loading == value) { + return; + } + m_loading = value; + emit loadingChanged(); +} + +void Lyrics::setLines(QVector lines, LyricsBackend::Backend source) { + std::sort(lines.begin(), lines.end(), [](const LyricLine& a, const LyricLine& b) { + return a.time < b.time; + }); + + m_lines = std::move(lines); + QStringList list; + list.reserve(m_lines.size()); + for (const auto& l : std::as_const(m_lines)) { + list.append(l.text); + } + m_lyrics = std::move(list); + + setBackend(source); + emit lyricsChanged(); + + const auto hasLyrics = !m_lines.isEmpty(); + if (hasLyrics != m_hasLyrics) { + m_hasLyrics = hasLyrics; + emit hasLyricsChanged(); + } +} + +void Lyrics::clearLines() { + // Doesn't actually clear lines, set a flag instead so anims can run + m_hasLyrics = false; + emit hasLyricsChanged(); +} + +void Lyrics::appendCandidates(const QList& add) { + if (add.isEmpty()) { + return; + } + bool changed = false; + for (const auto& c : add) { + if (!m_candidates.contains(c)) { + m_candidates.append(c); + changed = true; + } + } + if (changed) { + emit lyricCandidatesChanged(); + } +} + +void Lyrics::clearCandidates() { + if (m_candidates.isEmpty()) { + return; + } + m_candidates.clear(); + emit lyricCandidatesChanged(); +} + +void Lyrics::scheduleLoad() { + m_loadDebounce->start(); +} + +int Lyrics::newRequestId() { + return ++m_currentRequestId; +} + +void Lyrics::cancelInFlight() { + for (auto it = m_pendingReplies.begin(); it != m_pendingReplies.end(); ++it) { + for (auto& ptr : it.value()) { + if (auto* reply = ptr.data()) { + reply->abort(); + reply->deleteLater(); + } + } + } + m_pendingReplies.clear(); +} + +void Lyrics::trackReply(int reqId, QNetworkReply* reply) { + if (!reply) { + return; + } + m_pendingReplies[reqId].append(QPointer(reply)); +} + +void Lyrics::doLoad() { + if (m_artist.isEmpty() && m_title.isEmpty()) { + clearLines(); + clearCandidates(); + setLoading(false); + return; + } + + cancelInFlight(); + const int reqId = newRequestId(); + + setLoading(true); + clearLines(); + clearCandidates(); + + // Restore per-track prefs (offset, last-selected backend/id) + m_settingFromPrefs = true; + const QJsonObject saved = m_lyricsMap.value(trackKey()).toObject(); + setOffset(saved.value(u"offset"_s).toDouble(0.0)); + LyricCandidate restored; + const QString savedBackendKey = saved.value(u"backend"_s).toString(); + const QString savedId = saved.value(u"id"_s).toString(); + if (!savedBackendKey.isEmpty() && !savedId.isEmpty()) { + restored = LyricCandidate(backendFromKey(savedBackendKey), savedId, m_title, m_artist, m_album, m_duration); + } + m_settingFromPrefs = false; + + // Always populate online candidates for the picker, regardless of preferred backend + searchLrclibCandidates(reqId); + searchNetEaseCandidates(reqId); + + if (restored.isValid()) { + // Honor saved selection for this track + m_settingFromPrefs = true; + setSelectedCandidate(restored); + m_settingFromPrefs = false; + return; + } + + // Primary attempt by preferred backend + switch (m_preferredBackend) { + case LyricsBackend::Local: + tryLocal(reqId); + break; + case LyricsBackend::LRCLIB: + tryLrclib(reqId); + break; + case LyricsBackend::NetEase: + tryNetEase(reqId); + break; + case LyricsBackend::Auto: + default: + tryLocal(reqId); + break; + } +} + +void Lyrics::chainNext(LyricsBackend::Backend just_failed, int reqId) { + if (m_preferredBackend != LyricsBackend::Auto) { + // Non-auto modes don't chain + setLoading(false); + return; + } + switch (just_failed) { + case LyricsBackend::Local: + tryLrclib(reqId); + return; + case LyricsBackend::LRCLIB: + tryNetEase(reqId); + return; + case LyricsBackend::NetEase: + default: + setLoading(false); + return; + } +} + +void Lyrics::tryLocal(int reqId) { + if (reqId != m_currentRequestId) { + return; + } + + setBackend(LyricsBackend::Local); + + const QString dir = lyricsDir(); + if (dir.isEmpty()) { + chainNext(LyricsBackend::Local, reqId); + return; + } + + const QString direct = tryReadLocalLrc(dir, m_artist, m_title); + if (!direct.isEmpty()) { + QFile f(direct); + if (f.open(QIODevice::ReadOnly)) { + const QString text = QString::fromUtf8(f.readAll()); + const auto lines = parseLrc(text); + if (!lines.isEmpty()) { + setLines(lines, LyricsBackend::Local); + appendCandidates( + { LyricCandidate(LyricsBackend::Local, direct, m_title, m_artist, m_album, m_duration) }); + m_selected = LyricCandidate(LyricsBackend::Local, direct, m_title, m_artist, m_album, m_duration); + emit selectedCandidateChanged(); + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } + setLoading(false); + return; + } + } + } + + const QString recursive = findLocalLrcRecursive(dir, m_artist, m_title); + if (!recursive.isEmpty()) { + QFile f(recursive); + if (f.open(QIODevice::ReadOnly)) { + const QString text = QString::fromUtf8(f.readAll()); + const auto lines = parseLrc(text); + if (!lines.isEmpty()) { + setLines(lines, LyricsBackend::Local); + appendCandidates( + { LyricCandidate(LyricsBackend::Local, recursive, m_title, m_artist, m_album, m_duration) }); + m_selected = LyricCandidate(LyricsBackend::Local, recursive, m_title, m_artist, m_album, m_duration); + emit selectedCandidateChanged(); + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } + setLoading(false); + return; + } + } + } + + qCDebug(lcLyrics) << "no local lrc for" << m_artist << "-" << m_title; + chainNext(LyricsBackend::Local, reqId); +} + +void Lyrics::tryLrclib(int reqId) { + if (reqId != m_currentRequestId) { + return; + } + + setBackend(LyricsBackend::LRCLIB); + + QUrl url(u"https://lrclib.net/api/get"_s); + QUrlQuery q; + q.addQueryItem(u"track_name"_s, m_title); + q.addQueryItem(u"artist_name"_s, m_artist); + if (!m_album.isEmpty()) { + q.addQueryItem(u"album_name"_s, m_album); + } + if (m_duration > 0) { + q.addQueryItem(u"duration"_s, QString::number(qRound(m_duration))); + } + url.setQuery(q); + + auto* reply = getJson(url, lrclibHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCDebug(lcLyrics) << "lrclib /get error:" << reply->errorString(); + chainNext(LyricsBackend::LRCLIB, reqId); + return; + } + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QJsonObject obj = doc.object(); + const QString synced = obj.value(u"syncedLyrics"_s).toString(); + const qint64 id = static_cast(obj.value(u"id"_s).toDouble()); + + if (synced.isEmpty()) { + qCDebug(lcLyrics) << "lrclib: no syncedLyrics for" << m_artist << "-" << m_title; + chainNext(LyricsBackend::LRCLIB, reqId); + return; + } + + const auto lines = parseLrc(synced); + if (lines.isEmpty()) { + chainNext(LyricsBackend::LRCLIB, reqId); + return; + } + + writeCachedLrc(LyricsBackend::LRCLIB, QString::number(id), synced); + setLines(lines, LyricsBackend::LRCLIB); + const LyricCandidate cand(LyricsBackend::LRCLIB, QString::number(id), obj.value(u"trackName"_s).toString(), + obj.value(u"artistName"_s).toString(), obj.value(u"albumName"_s).toString(), + obj.value(u"duration"_s).toDouble()); + appendCandidates({ cand }); + m_selected = cand; + emit selectedCandidateChanged(); + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } + setLoading(false); + }); +} + +void Lyrics::tryNetEase(int reqId) { + if (reqId != m_currentRequestId) { + return; + } + + setBackend(LyricsBackend::NetEase); + + // Reset cookies (LyricsBackend::NetEase rejects requests with stale cookies sometimes) + m_nam->setCookieJar(new QNetworkCookieJar(m_nam)); + + QUrl url(u"https://music.163.com/api/search/get"_s); + QUrlQuery q; + q.addQueryItem(u"s"_s, u"%1 %2"_s.arg(m_title, m_artist)); + q.addQueryItem(u"type"_s, u"1"_s); + q.addQueryItem(u"limit"_s, u"5"_s); + url.setQuery(q); + + auto* reply = getJson(url, netEaseHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCDebug(lcLyrics) << "netease /search error:" << reply->errorString(); + chainNext(LyricsBackend::NetEase, reqId); + return; + } + + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QJsonArray songs = doc.object().value(u"result"_s).toObject().value(u"songs"_s).toArray(); + + // Find best match by artist substring + qint64 bestId = -1; + for (const auto& v : songs) { + const QJsonObject s = v.toObject(); + const QJsonArray artists = s.value(u"artists"_s).toArray(); + if (artists.isEmpty()) { + continue; + } + const QString sArtist = artists.first().toObject().value(u"name"_s).toString(); + if (containsCi(m_artist, sArtist) || containsCi(sArtist, m_artist)) { + bestId = static_cast(s.value(u"id"_s).toDouble()); + break; + } + } + + if (bestId < 0) { + qCDebug(lcLyrics) << "netease: no artist match for" << m_artist << "-" << m_title; + chainNext(LyricsBackend::NetEase, reqId); + return; + } + + fetchNetEaseLyricsById(QString::number(bestId), reqId); + }); +} + +void Lyrics::searchLrclibCandidates(int reqId) { + QUrl url(u"https://lrclib.net/api/search"_s); + QUrlQuery q; + q.addQueryItem(u"track_name"_s, m_title); + q.addQueryItem(u"artist_name"_s, m_artist); + url.setQuery(q); + + auto* reply = getJson(url, lrclibHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCDebug(lcLyrics) << "lrclib /search error:" << reply->errorString(); + return; + } + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QJsonArray arr = doc.array(); + + QList add; + add.reserve(arr.size()); + for (const auto& v : arr) { + const QJsonObject o = v.toObject(); + if (o.value(u"syncedLyrics"_s).isNull() && o.value(u"plainLyrics"_s).isNull()) { + continue; + } + add.append( + LyricCandidate(LyricsBackend::LRCLIB, QString::number(static_cast(o.value(u"id"_s).toDouble())), + o.value(u"trackName"_s).toString(), o.value(u"artistName"_s).toString(), + o.value(u"albumName"_s).toString(), o.value(u"duration"_s).toDouble())); + } + appendCandidates(add); + }); +} + +void Lyrics::searchNetEaseCandidates(int reqId) { + m_nam->setCookieJar(new QNetworkCookieJar(m_nam)); + + QUrl url(u"https://music.163.com/api/search/get"_s); + QUrlQuery q; + q.addQueryItem(u"s"_s, u"%1 %2"_s.arg(m_title, m_artist)); + q.addQueryItem(u"type"_s, u"1"_s); + q.addQueryItem(u"limit"_s, u"5"_s); + url.setQuery(q); + + auto* reply = getJson(url, netEaseHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCDebug(lcLyrics) << "netease candidates error:" << reply->errorString(); + return; + } + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QJsonArray songs = doc.object().value(u"result"_s).toObject().value(u"songs"_s).toArray(); + + QList add; + add.reserve(songs.size()); + for (const auto& v : songs) { + const QJsonObject s = v.toObject(); + QStringList artistNames; + const QJsonArray artists = s.value(u"artists"_s).toArray(); + artistNames.reserve(artists.size()); + for (const auto& a : artists) { + artistNames.append(a.toObject().value(u"name"_s).toString()); + } + add.append(LyricCandidate(LyricsBackend::NetEase, + QString::number(static_cast(s.value(u"id"_s).toDouble())), s.value(u"name"_s).toString(), + artistNames.join(u", "_s))); + } + appendCandidates(add); + }); +} + +void Lyrics::fetchLrclibById(const QString& id, int reqId) { + QUrl url(u"https://lrclib.net/api/get/"_s + id); + auto* reply = getJson(url, lrclibHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId, id] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCWarning(lcLyrics) << "lrclib /get/{id} error:" << reply->errorString(); + setLoading(false); + return; + } + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QString synced = doc.object().value(u"syncedLyrics"_s).toString(); + if (synced.isEmpty()) { + qCDebug(lcLyrics) << "lrclib /get/{id}: no syncedLyrics"; + setLoading(false); + return; + } + writeCachedLrc(LyricsBackend::LRCLIB, id, synced); + setLines(parseLrc(synced), LyricsBackend::LRCLIB); + setLoading(false); + }); +} + +void Lyrics::fetchNetEaseLyricsById(const QString& id, int reqId) { + QUrl url(u"https://music.163.com/api/song/lyric"_s); + QUrlQuery q; + q.addQueryItem(u"id"_s, id); + q.addQueryItem(u"lv"_s, u"1"_s); + q.addQueryItem(u"kv"_s, u"1"_s); + q.addQueryItem(u"tv"_s, u"-1"_s); + url.setQuery(q); + + auto* reply = getJson(url, netEaseHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId, id] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCWarning(lcLyrics) << "netease /lyric error:" << reply->errorString(); + setLoading(false); + return; + } + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QString lrc = doc.object().value(u"lrc"_s).toObject().value(u"lyric"_s).toString(); + if (lrc.isEmpty()) { + qCDebug(lcLyrics) << "netease /lyric: empty for id" << id; + setLoading(false); + return; + } + writeCachedLrc(LyricsBackend::NetEase, id, lrc); + setLines(parseLrc(lrc), LyricsBackend::NetEase); + setLoading(false); + }); +} + +QNetworkReply* Lyrics::getJson(const QUrl& url, const QHash& headers) { + QNetworkRequest req(url); + req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + req.setRawHeader("Cache-Control"_ba, "no-cache, no-store"_ba); + req.setRawHeader("Pragma"_ba, "no-cache"_ba); + req.setRawHeader("Connection"_ba, "close"_ba); + req.setRawHeader("Accept"_ba, "application/json"_ba); + for (auto it = headers.constBegin(); it != headers.constEnd(); ++it) { + req.setRawHeader(it.key(), it.value()); + } + return m_nam->get(req); +} + +void Lyrics::onPreferredBackendConfigChanged() { + auto* svcCfg = config::GlobalConfig::instance()->services(); + const LyricsBackend::Backend desired = backendFromKey(svcCfg->lyricsBackend()); + if (desired == m_preferredBackend) { + return; + } + m_preferredBackend = desired; + emit preferredBackendChanged(); + scheduleLoad(); +} + +void Lyrics::onLyricsDirChanged() { + loadLyricsMap(); + scheduleLoad(); +} + +void Lyrics::loadLyricsMap() { + m_lyricsMap = {}; + m_lyricsMapLoaded = false; + + QFile f(lyricsMapPath()); + if (!f.open(QIODevice::ReadOnly)) { + m_lyricsMapLoaded = true; + return; + } + const QByteArray bytes = f.readAll(); + f.close(); + + QJsonParseError err{}; + const QJsonDocument doc = QJsonDocument::fromJson(bytes, &err); + if (err.error != QJsonParseError::NoError) { + qCWarning(lcLyrics) << "lyrics_map.json parse error:" << err.errorString(); + m_lyricsMapLoaded = true; + return; + } + m_lyricsMap = doc.object(); + m_lyricsMapLoaded = true; +} + +void Lyrics::persistTrackPrefs() { + if (!m_lyricsMapLoaded || trackKey().isEmpty()) { + return; + } + const QString key = trackKey(); + QJsonObject entry = m_lyricsMap.value(key).toObject(); + entry.insert(u"offset"_s, m_offset); + if (m_selected.isValid()) { + entry.insert(u"backend"_s, backendKey(m_selected.backend())); + entry.insert(u"id"_s, m_selected.id()); + } + m_lyricsMap.insert(key, entry); + + const QString dir = lyricsDir(); + if (dir.isEmpty()) { + return; + } + QDir().mkpath(dir); + + QSaveFile out(lyricsMapPath()); + if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qCWarning(lcLyrics) << "cannot open" << lyricsMapPath() << "for write:" << out.errorString(); + return; + } + const QByteArray bytes = QJsonDocument(m_lyricsMap).toJson(QJsonDocument::Compact); + if (out.write(bytes) != bytes.size()) { + qCWarning(lcLyrics) << "short write to" << lyricsMapPath(); + out.cancelWriting(); + return; + } + if (!out.commit()) { + qCWarning(lcLyrics) << "commit failed for" << lyricsMapPath() << ":" << out.errorString(); + } +} + +QString Lyrics::lyricsDir() const { + QString dir = config::GlobalConfig::instance()->paths()->lyricsDir(); + if (dir.isEmpty()) { + return {}; + } + while (dir.endsWith(QLatin1Char('/')) && dir.size() > 1) { + dir.chop(1); + } + return dir; +} + +QString Lyrics::lyricsMapPath() const { + const QString dir = lyricsDir(); + return dir.isEmpty() ? QString() : dir + u"/lyrics_map.json"_s; +} + +QString Lyrics::trackKey() const { + if (m_artist.isEmpty() && m_title.isEmpty()) { + return {}; + } + return u"%1 - %2"_s.arg(joinArtists(m_artist), m_title); +} + +QString Lyrics::backendKey(LyricsBackend::Backend value) { + switch (value) { + case LyricsBackend::Local: + return u"LyricsBackend::Local"_s; + case LyricsBackend::LRCLIB: + return u"LyricsBackend::LRCLIB"_s; + case LyricsBackend::NetEase: + return u"LyricsBackend::NetEase"_s; + case LyricsBackend::Auto: + default: + return u"LyricsBackend::Auto"_s; + } +} + +LyricsBackend::Backend Lyrics::backendFromKey(const QString& key) { + if (key.compare(u"LyricsBackend::Local"_s, Qt::CaseInsensitive) == 0) { + return LyricsBackend::Local; + } + if (key.compare(u"LyricsBackend::LRCLIB"_s, Qt::CaseInsensitive) == 0) { + return LyricsBackend::LRCLIB; + } + if (key.compare(u"LyricsBackend::NetEase"_s, Qt::CaseInsensitive) == 0) { + return LyricsBackend::NetEase; + } + return LyricsBackend::Auto; +} + +const QString& Lyrics::cacheDir() { + static const QString s_dir = [] { + QString cache = qEnvironmentVariable("XDG_CACHE_HOME"); + if (cache.isEmpty()) { + cache = QDir::homePath() + u"/.cache"_s; + } + return cache + u"/caelestia/lyrics"_s; + }(); + return s_dir; +} + +QString Lyrics::cachePathFor(LyricsBackend::Backend backend, const QString& id) { + if (id.isEmpty() || backend == LyricsBackend::Auto || backend == LyricsBackend::Local) { + return {}; + } + return u"%1/%2/%3.lrc"_s.arg(cacheDir(), backendKey(backend), sanitizeFilenamePart(id)); +} + +QString Lyrics::readCachedLrc(LyricsBackend::Backend backend, const QString& id) { + const QString path = cachePathFor(backend, id); + if (path.isEmpty()) { + return {}; + } + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) { + return {}; + } + return QString::fromUtf8(f.readAll()); +} + +void Lyrics::writeCachedLrc(LyricsBackend::Backend backend, const QString& id, const QString& text) { + if (text.isEmpty()) { + return; + } + const QString path = cachePathFor(backend, id); + if (path.isEmpty()) { + return; + } + QDir().mkpath(QFileInfo(path).absolutePath()); + + QSaveFile out(path); + if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qCWarning(lcLyrics) << "cannot open" << path << "for write:" << out.errorString(); + return; + } + const QByteArray bytes = text.toUtf8(); + if (out.write(bytes) != bytes.size()) { + qCWarning(lcLyrics) << "short write to" << path; + out.cancelWriting(); + return; + } + if (!out.commit()) { + qCWarning(lcLyrics) << "commit failed for" << path << ":" << out.errorString(); + } +} + +QString Lyrics::tryReadLocalLrc(const QString& dir, const QString& artist, const QString& title) { + if (artist.isEmpty() && title.isEmpty()) { + return {}; + } + const QString flat = u"%1/%2 - %3.lrc"_s.arg(dir, sanitizeFilenamePart(artist), sanitizeFilenamePart(title)); + return QFile::exists(flat) ? flat : QString(); +} + +QString Lyrics::findLocalLrcRecursive(const QString& dir, const QString& artist, const QString& title) { + if (dir.isEmpty()) { + return {}; + } + if (artist.isEmpty() && title.isEmpty()) { + return {}; + } + + QDirIterator it(dir, QStringList{ u"*.lrc"_s }, QDir::Files | QDir::NoDotAndDotDot, + QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); + + while (it.hasNext()) { + const QString path = it.next(); + const QString name = it.fileName(); + if ((artist.isEmpty() || containsCi(name, artist)) && (title.isEmpty() || containsCi(name, title))) { + return path; + } + } + return {}; +} + +QVector Lyrics::parseLrc(const QString& text) { + QVector result; + if (text.isEmpty()) { + return result; + } + + static const QRegularExpression timeRegex(u"\\[(\\d+):(\\d+(?:\\.\\d+)?)\\]"_s); + static const QStringList creditKeywords = { + u"作词"_s, + u"作曲"_s, + u"编曲"_s, + u"制作"_s, + u"收录"_s, + u"演奏"_s, + u"词:"_s, + u"曲:"_s, + u"Lyricist"_s, + u"Composer"_s, + u"Arranger"_s, + u"Producer"_s, + u"Mixing"_s, + u"Mastering"_s, + }; + + const QStringList lines = text.split(QLatin1Char('\n')); + for (const QString& line : lines) { + QList matches; + auto it = timeRegex.globalMatch(line); + while (it.hasNext()) { + matches.append(it.next()); + } + if (matches.isEmpty()) { + continue; + } + + QString lyric = line; + lyric.replace(timeRegex, QString()); + lyric = lyric.trimmed(); + + const qreal firstTime = matches.first().captured(1).toInt() * 60.0 + matches.first().captured(2).toDouble(); + + if (firstTime < 20.0) { + bool isCredit = false; + for (const QString& k : creditKeywords) { + if (lyric.contains(k, Qt::CaseInsensitive)) { + isCredit = true; + break; + } + } + if (isCredit && (lyric.contains(QLatin1Char(':')) || lyric.contains(QChar(0xFF1A)) || lyric.size() < 25)) { + continue; + } + } + + for (const auto& m : matches) { + const qreal t = m.captured(1).toInt() * 60.0 + m.captured(2).toDouble(); + result.append(LyricLine{ t, lyric }); + } + } + + std::sort(result.begin(), result.end(), [](const LyricLine& a, const LyricLine& b) { + return a.time < b.time; + }); + + return result; +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/lyrics.hpp b/plugin/src/Caelestia/Services/lyrics.hpp new file mode 100644 index 00000000..e06e8696 --- /dev/null +++ b/plugin/src/Caelestia/Services/lyrics.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include "lyriccandidate.hpp" + +#include +#include +#include +#include + +namespace caelestia::services { + +struct LyricLine { + qreal time = 0.0; + QString text; +}; + +class Lyrics : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QStringList lyrics READ lyrics NOTIFY lyricsChanged) + Q_PROPERTY(caelestia::services::LyricsBackend::Backend backend READ backend NOTIFY backendChanged) + Q_PROPERTY(caelestia::services::LyricsBackend::Backend preferredBackend READ preferredBackend WRITE + setPreferredBackend NOTIFY preferredBackendChanged) + Q_PROPERTY( + QList lyricCandidates READ lyricCandidates NOTIFY lyricCandidatesChanged) + Q_PROPERTY(caelestia::services::LyricCandidate selectedCandidate READ selectedCandidate WRITE setSelectedCandidate + NOTIFY selectedCandidateChanged) + Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) + Q_PROPERTY(bool hasLyrics READ hasLyrics NOTIFY lyricsChanged) + Q_PROPERTY(qreal offset READ offset WRITE setOffset NOTIFY offsetChanged) + Q_PROPERTY(QString trackArtist READ trackArtist NOTIFY trackChanged) + Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY trackChanged) + +public: + explicit Lyrics(QObject* parent = nullptr); + + [[nodiscard]] QStringList lyrics() const; + [[nodiscard]] LyricsBackend::Backend backend() const; + [[nodiscard]] LyricsBackend::Backend preferredBackend() const; + void setPreferredBackend(LyricsBackend::Backend value); + [[nodiscard]] QList lyricCandidates() const; + [[nodiscard]] LyricCandidate selectedCandidate() const; + void setSelectedCandidate(const LyricCandidate& value); + [[nodiscard]] bool loading() const; + [[nodiscard]] bool hasLyrics() const; + [[nodiscard]] qreal offset() const; + void setOffset(qreal value); + [[nodiscard]] QString trackArtist() const; + [[nodiscard]] QString trackTitle() const; + + [[nodiscard]] Q_INVOKABLE int indexForTime(qreal time) const; + Q_INVOKABLE void setTrack( + const QString& artist, const QString& title, const QString& album = {}, qreal duration = 0.0); + Q_INVOKABLE void clearTrack(); + Q_INVOKABLE void refresh(); + +signals: + void lyricsChanged(); + void backendChanged(); + void preferredBackendChanged(); + void lyricCandidatesChanged(); + void selectedCandidateChanged(); + void loadingChanged(); + void hasLyricsChanged(); + void offsetChanged(); + void trackChanged(); + +private: + void setBackend(LyricsBackend::Backend value); + void setLoading(bool value); + void setLines(QVector lines, LyricsBackend::Backend source); + void clearLines(); + void appendCandidates(const QList& add); + void clearCandidates(); + + void scheduleLoad(); + void doLoad(); + void cancelInFlight(); + int newRequestId(); + + void tryLocal(int reqId); + void tryLrclib(int reqId); + void tryNetEase(int reqId); + void chainNext(LyricsBackend::Backend just_failed, int reqId); + + void searchLrclibCandidates(int reqId); + void searchNetEaseCandidates(int reqId); + + void fetchLrclibById(const QString& id, int reqId); + void fetchNetEaseLyricsById(const QString& id, int reqId); + + QNetworkReply* getJson(const QUrl& url, const QHash& headers = {}); + void trackReply(int reqId, QNetworkReply* reply); + + void onPreferredBackendConfigChanged(); + void onLyricsDirChanged(); + + void loadLyricsMap(); + void persistTrackPrefs(); + + [[nodiscard]] QString lyricsDir() const; + [[nodiscard]] QString lyricsMapPath() const; + [[nodiscard]] QString trackKey() const; + [[nodiscard]] static QString backendKey(LyricsBackend::Backend value); + [[nodiscard]] static LyricsBackend::Backend backendFromKey(const QString& key); + + [[nodiscard]] static const QString& cacheDir(); + [[nodiscard]] static QString cachePathFor(LyricsBackend::Backend backend, const QString& id); + [[nodiscard]] static QString readCachedLrc(LyricsBackend::Backend backend, const QString& id); + static void writeCachedLrc(LyricsBackend::Backend backend, const QString& id, const QString& text); + + [[nodiscard]] static QVector parseLrc(const QString& text); + [[nodiscard]] static QString tryReadLocalLrc(const QString& dir, const QString& artist, const QString& title); + [[nodiscard]] static QString findLocalLrcRecursive(const QString& dir, const QString& artist, const QString& title); + + QNetworkAccessManager* m_nam; + QTimer* m_loadDebounce; + + QVector m_lines; + QStringList m_lyrics; + LyricsBackend::Backend m_backend = LyricsBackend::Auto; + LyricsBackend::Backend m_preferredBackend = LyricsBackend::Auto; + QList m_candidates; + LyricCandidate m_selected; + bool m_loading = false; + bool m_hasLyrics = false; + qreal m_offset = 0.0; + + QString m_artist; + QString m_title; + QString m_album; + qreal m_duration = 0.0; + + int m_currentRequestId = 0; + QHash>> m_pendingReplies; + + QJsonObject m_lyricsMap; + bool m_lyricsMapLoaded = false; + bool m_settingFromPrefs = false; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/memory.cpp b/plugin/src/Caelestia/Services/memory.cpp new file mode 100644 index 00000000..e4df3133 --- /dev/null +++ b/plugin/src/Caelestia/Services/memory.cpp @@ -0,0 +1,58 @@ +#include "memory.hpp" + +#include +#include + +namespace caelestia::services { + +Memory::Memory(QObject* parent) + : TickingService(parent) {} + +qreal Memory::used() const { + return m_used; +} + +qreal Memory::total() const { + return m_total; +} + +qreal Memory::percentage() const { + return m_total > 0.0 ? m_used / m_total : 0.0; +} + +void Memory::tick() { + QFile f(QStringLiteral("/proc/meminfo")); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return; + } + const QByteArray data = f.readAll(); + f.close(); + + static const QRegularExpression reTotal(QStringLiteral("MemTotal: *(\\d+)")); + static const QRegularExpression reAvail(QStringLiteral("MemAvailable: *(\\d+)")); + const QString text = QString::fromLatin1(data); + + const auto totalMatch = reTotal.match(text); + const auto availMatch = reAvail.match(text); + if (!totalMatch.hasMatch() || !availMatch.hasMatch()) { + return; + } + + const quint64 totalKib = totalMatch.captured(1).toULongLong(); + const quint64 availKib = availMatch.captured(1).toULongLong(); + if (totalKib == 0) { + return; + } + const quint64 usedKib = totalKib > availKib ? totalKib - availKib : 0; + + if (totalKib == m_lastTotal && usedKib == m_lastUsed) { + return; + } + m_lastTotal = totalKib; + m_lastUsed = usedKib; + m_total = static_cast(totalKib); + m_used = static_cast(usedKib); + Q_EMIT changed(); +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/memory.hpp b/plugin/src/Caelestia/Services/memory.hpp new file mode 100644 index 00000000..4c8950c2 --- /dev/null +++ b/plugin/src/Caelestia/Services/memory.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "tickingservice.hpp" + +#include +#include + +namespace caelestia::services { + +class Memory : public TickingService { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(qreal used READ used NOTIFY changed) + Q_PROPERTY(qreal total READ total NOTIFY changed) + Q_PROPERTY(qreal percentage READ percentage NOTIFY changed) + +public: + explicit Memory(QObject* parent = nullptr); + + [[nodiscard]] qreal used() const; + [[nodiscard]] qreal total() const; + [[nodiscard]] qreal percentage() const; + +signals: + void changed(); + +protected: + void tick() override; + +private: + qreal m_used = 0.0; + qreal m_total = 1.0; + quint64 m_lastUsed = 0; + quint64 m_lastTotal = 0; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/sensorslib.cpp b/plugin/src/Caelestia/Services/sensorslib.cpp new file mode 100644 index 00000000..7b57731b --- /dev/null +++ b/plugin/src/Caelestia/Services/sensorslib.cpp @@ -0,0 +1,166 @@ +#include "sensorslib.hpp" + +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcSensorsLib, "caelestia.services.sensorslib", QtInfoMsg) + +namespace caelestia::services::sensorslib { + +namespace { + +std::atomic g_initOk{ false }; +std::once_flag g_initFlag; + +void doInit() { + if (sensors_init(nullptr) != 0) { + qCWarning(lcSensorsLib, "sensors_init failed"); + g_initOk.store(false, std::memory_order_release); + return; + } + g_initOk.store(true, std::memory_order_release); + std::atexit([] { + if (g_initOk.load(std::memory_order_acquire)) { + sensors_cleanup(); + } + }); +} + +[[nodiscard]] std::optional readTempInput(const sensors_chip_name* chip, const sensors_feature* feat) { + const sensors_subfeature* sf = sensors_get_subfeature(chip, feat, SENSORS_SUBFEATURE_TEMP_INPUT); + if (!sf) { + return std::nullopt; + } + double value = 0.0; + if (sensors_get_value(chip, sf->number, &value) != 0) { + return std::nullopt; + } + return value; +} + +[[nodiscard]] QByteArray featureLabel(const sensors_chip_name* chip, const sensors_feature* feat) { + char* raw = sensors_get_label(chip, feat); + if (!raw) { + return {}; + } + QByteArray out(raw); + std::free(raw); + return out; +} + +bool labelEquals(const QByteArray& label, const char* literal) { + return label == QByteArrayView(literal); +} + +bool labelStartsWith(const QByteArray& label, const char* prefix) { + const auto n = std::strlen(prefix); + return static_cast(label.size()) >= n && std::memcmp(label.constData(), prefix, n) == 0; +} + +} // namespace + +void ensureInit() { + std::call_once(g_initFlag, doInit); +} + +std::optional cpuPackageTemp() { + ensureInit(); + if (!g_initOk.load(std::memory_order_acquire)) { + return std::nullopt; + } + + std::optional primary; // Package id N / Tdie + std::optional fallback; // Tctl + + int chipNr = 0; + while (const sensors_chip_name* chip = sensors_get_detected_chips(nullptr, &chipNr)) { + int featNr = 0; + while (const sensors_feature* feat = sensors_get_features(chip, &featNr)) { + if (feat->type != SENSORS_FEATURE_TEMP) { + continue; + } + const QByteArray label = featureLabel(chip, feat); + if (label.isEmpty()) { + continue; + } + + if (labelStartsWith(label, "Package id ") || labelEquals(label, "Tdie")) { + if (auto v = readTempInput(chip, feat)) { + primary = v; + } + } else if (labelEquals(label, "Tctl")) { + if (auto v = readTempInput(chip, feat)) { + fallback = v; + } + } + } + } + + return primary.has_value() ? primary : fallback; +} + +std::optional gpuPciAverageTemp() { + ensureInit(); + if (!g_initOk.load(std::memory_order_acquire)) { + return std::nullopt; + } + + double sumPrimary = 0.0; + int countPrimary = 0; + double sumFallback = 0.0; + int countFallback = 0; + + int chipNr = 0; + while (const sensors_chip_name* chip = sensors_get_detected_chips(nullptr, &chipNr)) { + if (chip->bus.type != SENSORS_BUS_TYPE_PCI) { + continue; + } + + int featNr = 0; + while (const sensors_feature* feat = sensors_get_features(chip, &featNr)) { + if (feat->type != SENSORS_FEATURE_TEMP) { + continue; + } + const QByteArray label = featureLabel(chip, feat); + if (label.isEmpty()) { + continue; + } + + const bool tempIndexed = labelStartsWith(label, "temp") && label.size() > 4 && + std::isdigit(static_cast(label[4])); + const bool isPrimary = tempIndexed || labelEquals(label, "GPU core") || labelEquals(label, "edge"); + const bool isFallback = labelEquals(label, "junction") || labelEquals(label, "mem"); + + if (!isPrimary && !isFallback) { + continue; + } + + const auto v = readTempInput(chip, feat); + if (!v) { + continue; + } + if (isPrimary) { + sumPrimary += *v; + ++countPrimary; + } else { + sumFallback += *v; + ++countFallback; + } + } + } + + if (countPrimary > 0) { + return sumPrimary / countPrimary; + } + if (countFallback > 0) { + return sumFallback / countFallback; + } + return std::nullopt; +} + +} // namespace caelestia::services::sensorslib diff --git a/plugin/src/Caelestia/Services/sensorslib.hpp b/plugin/src/Caelestia/Services/sensorslib.hpp new file mode 100644 index 00000000..8c305a6f --- /dev/null +++ b/plugin/src/Caelestia/Services/sensorslib.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace caelestia::services::sensorslib { + +void ensureInit(); + +[[nodiscard]] std::optional cpuPackageTemp(); +[[nodiscard]] std::optional gpuPciAverageTemp(); + +} // namespace caelestia::services::sensorslib diff --git a/plugin/src/Caelestia/Services/storage.cpp b/plugin/src/Caelestia/Services/storage.cpp new file mode 100644 index 00000000..d3e31496 --- /dev/null +++ b/plugin/src/Caelestia/Services/storage.cpp @@ -0,0 +1,312 @@ +#include "storage.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcStorage, "caelestia.services.storage", QtInfoMsg) + +namespace caelestia::services { + +namespace { + +struct Accum { + quint64 usedBytes = 0; + quint64 totalBytes = 0; + bool hasRoot = false; +}; + +[[nodiscard]] QString sysfsRealPath(uint major, uint minor) { + const QString link = QStringLiteral("/sys/dev/block/%1:%2").arg(major).arg(minor); + const QString resolved = QFileInfo(link).canonicalFilePath(); + return resolved; +} + +[[nodiscard]] bool readDevtFromSysfs(const QString& sysfsBlockDir, uint& major, uint& minor) { + QFile f(sysfsBlockDir + QStringLiteral("/dev")); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return false; + } + const QByteArray line = f.readLine().trimmed(); + f.close(); + + const qsizetype colon = line.indexOf(':'); + if (colon <= 0) { + return false; + } + bool okM = false; + bool okN = false; + major = line.left(colon).toUInt(&okM); + minor = line.mid(colon + 1).toUInt(&okN); + return okM && okN; +} + +QStringList resolveByDevt(uint major, uint minor, int depth = 0); + +QStringList resolveAtNode(const QString& node, int depth) { + if (node.isEmpty() || depth > 8) { + return {}; + } + + const QFileInfo nodeInfo(node); + if (!nodeInfo.exists() || !nodeInfo.isDir()) { + return {}; + } + + if (QFileInfo::exists(node + QStringLiteral("/partition"))) { + const QString diskNode = nodeInfo.path(); + return { QFileInfo(diskNode).fileName() }; + } + + const QDir slavesDir(node + QStringLiteral("/slaves")); + if (slavesDir.exists()) { + const QStringList slaves = slavesDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + if (!slaves.isEmpty()) { + QStringList out; + for (const QString& slave : slaves) { + uint sm = 0; + uint sn = 0; + const QString slaveDir = QStringLiteral("/sys/class/block/") + slave; + if (!readDevtFromSysfs(slaveDir, sm, sn)) { + continue; + } + const auto devs = resolveByDevt(sm, sn, depth + 1); + for (const QString& d : devs) { + if (!out.contains(d)) { + out.append(d); + } + } + } + return out; + } + } + + return { nodeInfo.fileName() }; +} + +QStringList resolveByDevt(uint major, uint minor, int depth) { + return resolveAtNode(sysfsRealPath(major, minor), depth); +} + +} // namespace + +Storage::Storage(QObject* parent) + : TickingService(parent) {} + +qreal Storage::percentage() const { + qreal totalUsed = 0.0; + qreal totalSize = 0.0; + for (const DiskInfo* d : m_disks) { + totalUsed += d->used(); + totalSize += d->total(); + } + return totalSize > 0.0 ? totalUsed / totalSize : 0.0; +} + +bool Storage::sameOrder(const QList& a, const QList& b) { + if (a.size() != b.size()) { + return false; + } + for (qsizetype i = 0; i < a.size(); ++i) { + if (a.at(i) != b.at(i)) { + return false; + } + } + return true; +} + +QQmlListProperty Storage::disksProp() { + return QQmlListProperty(this, nullptr, &Storage::disksCount, &Storage::disksAt); +} + +qsizetype Storage::disksCount(QQmlListProperty* prop) { + return static_cast(prop->object)->m_disks.size(); +} + +DiskInfo* Storage::disksAt(QQmlListProperty* prop, qsizetype i) { + return static_cast(prop->object)->m_disks.at(i); +} + +DiskInfo* Storage::manualPrimaryDisk() const { + return m_manualPrimaryDisk.data(); +} + +void Storage::setManualPrimaryDisk(DiskInfo* disk) { + if (m_manualPrimaryDisk.data() == disk) { + return; + } + m_manualPrimaryDisk = disk; + Q_EMIT manualPrimaryDiskChanged(); + Q_EMIT primaryDiskChanged(); +} + +DiskInfo* Storage::primaryDisk() const { + if (auto* m = m_manualPrimaryDisk.data()) { + return m; + } + return m_disks.isEmpty() ? nullptr : m_disks.first(); +} + +bool Storage::isPseudoFs(QByteArrayView fsType) { + static constexpr const char* kPseudo[] = { + "tmpfs", + "devtmpfs", + "proc", + "sysfs", + "cgroup", + "cgroup2", + "overlay", + "squashfs", + "devpts", + "mqueue", + "ramfs", + "rpc_pipefs", + "autofs", + "configfs", + "debugfs", + "tracefs", + "securityfs", + "pstore", + "bpf", + "binfmt_misc", + "hugetlbfs", + "fusectl", + "efivarfs", + "selinuxfs", + }; + for (const char* p : kPseudo) { + if (fsType == QByteArrayView(p)) { + return true; + } + } + return fsType.startsWith(QByteArrayView("fuse.")); +} + +QStringList Storage::resolveToPhysicalDisks(const QString& devicePath) { + if (devicePath.isEmpty() || !devicePath.startsWith(QLatin1Char('/'))) { + return {}; + } + struct stat st{}; + if (::stat(devicePath.toLocal8Bit().constData(), &st) != 0) { + return {}; + } + if (!S_ISBLK(st.st_mode)) { + return {}; + } + return resolveByDevt(major(st.st_rdev), minor(st.st_rdev)); +} + +void Storage::tick() { + const qreal prevPercentage = percentage(); + QHash byDisk; + + // Multiple mounts can share a single backing filesystem (btrfs subvolumes, + // bind mounts, etc.) and each one reports identical bytesTotal/bytesAvailable. + // Dedupe by source device so the filesystem only contributes once per disk. + struct DeviceEntry { + quint64 totalBytes = 0; + quint64 usedBytes = 0; + bool hasRoot = false; + QByteArray device; + }; + + QHash byDevice; + + const auto mountedVols = QStorageInfo::mountedVolumes(); + for (const QStorageInfo& v : mountedVols) { + if (!v.isReady() || !v.isValid() || v.bytesTotal() <= 0) { + continue; + } + if (isPseudoFs(QByteArrayView(v.fileSystemType()))) { + continue; + } + + const QByteArray device = v.device(); + const auto totalBytes = static_cast(v.bytesTotal()); + const auto availBytes = static_cast(v.bytesAvailable()); + const quint64 usedBytes = totalBytes > availBytes ? totalBytes - availBytes : 0; + const bool isRoot = v.rootPath() == QStringLiteral("/"); + + DeviceEntry& e = byDevice[device]; + e.device = device; + e.totalBytes = totalBytes; + e.usedBytes = usedBytes; + e.hasRoot = e.hasRoot || isRoot; + } + + for (auto it = byDevice.constBegin(); it != byDevice.constEnd(); ++it) { + const DeviceEntry& e = it.value(); + const QStringList disks = resolveToPhysicalDisks(QString::fromLocal8Bit(e.device)); + if (disks.isEmpty()) { + continue; + } + for (const QString& d : disks) { + if (d.startsWith(QStringLiteral("zram"))) { + continue; + } + Accum& a = byDisk[d]; + a.usedBytes += e.usedBytes; + a.totalBytes += e.totalBytes; + a.hasRoot = a.hasRoot || e.hasRoot; + } + } + + QHash existing; + existing.reserve(m_disks.size()); + for (DiskInfo* d : std::as_const(m_disks)) { + existing.insert(d->mount(), d); + } + + QList next; + next.reserve(byDisk.size()); + for (auto it = byDisk.constBegin(); it != byDisk.constEnd(); ++it) { + if (DiskInfo* survivor = existing.take(it.key())) { + survivor->update(it.value().usedBytes, it.value().totalBytes, it.value().hasRoot); + next.append(survivor); + } else { + next.append(new DiskInfo(it.key(), it.value().usedBytes, it.value().totalBytes, it.value().hasRoot, this)); + } + } + + std::sort(next.begin(), next.end(), [](const DiskInfo* a, const DiskInfo* b) { + if (a->hasRoot() != b->hasRoot()) { + return a->hasRoot(); + } + return a->mount() < b->mount(); + }); + + bool manualCleared = false; + if (DiskInfo* m = m_manualPrimaryDisk.data(); m && existing.contains(m->mount())) { + m_manualPrimaryDisk.clear(); + manualCleared = true; + } + for (DiskInfo* stale : std::as_const(existing)) { + stale->deleteLater(); + } + + const bool listChanged = !sameOrder(m_disks, next); + DiskInfo* prevPrimary = primaryDisk(); + m_disks = next; + + if (listChanged) { + Q_EMIT disksChanged(); + } + if (std::abs(percentage() - prevPercentage) > 0.0001) { + Q_EMIT percentageChanged(); + } + if (manualCleared) { + Q_EMIT manualPrimaryDiskChanged(); + } + if (primaryDisk() != prevPrimary) { + Q_EMIT primaryDiskChanged(); + } +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/storage.hpp b/plugin/src/Caelestia/Services/storage.hpp new file mode 100644 index 00000000..a3ee6beb --- /dev/null +++ b/plugin/src/Caelestia/Services/storage.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "diskinfo.hpp" +#include "tickingservice.hpp" + +#include +#include +#include +#include + +namespace caelestia::services { + +class Storage : public TickingService { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged) + Q_PROPERTY(QQmlListProperty disks READ disksProp NOTIFY disksChanged) + Q_PROPERTY(caelestia::services::DiskInfo* manualPrimaryDisk READ manualPrimaryDisk WRITE setManualPrimaryDisk NOTIFY + manualPrimaryDiskChanged) + Q_PROPERTY(caelestia::services::DiskInfo* primaryDisk READ primaryDisk NOTIFY primaryDiskChanged) + +public: + explicit Storage(QObject* parent = nullptr); + + [[nodiscard]] qreal percentage() const; + [[nodiscard]] QQmlListProperty disksProp(); + [[nodiscard]] DiskInfo* manualPrimaryDisk() const; + void setManualPrimaryDisk(DiskInfo* disk); + [[nodiscard]] DiskInfo* primaryDisk() const; + +signals: + void disksChanged(); + void percentageChanged(); + void manualPrimaryDiskChanged(); + void primaryDiskChanged(); + +protected: + void tick() override; + +private: + [[nodiscard]] static QStringList resolveToPhysicalDisks(const QString& devicePath); + [[nodiscard]] static bool isPseudoFs(QByteArrayView fsType); + [[nodiscard]] static bool sameOrder(const QList& a, const QList& b); + + static qsizetype disksCount(QQmlListProperty* prop); + static DiskInfo* disksAt(QQmlListProperty* prop, qsizetype i); + + QList m_disks; + QPointer m_manualPrimaryDisk; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/tickingservice.cpp b/plugin/src/Caelestia/Services/tickingservice.cpp new file mode 100644 index 00000000..78454f5e --- /dev/null +++ b/plugin/src/Caelestia/Services/tickingservice.cpp @@ -0,0 +1,51 @@ +#include "tickingservice.hpp" + +#include "../Config/config.hpp" +#include "../Config/dashboardconfig.hpp" + +namespace caelestia::services { + +TickingService::TickingService(QObject* parent) + : Service(parent) + , m_timer(new QTimer(this)) { + m_timer->setSingleShot(false); + QObject::connect(m_timer, &QTimer::timeout, this, [this] { + tick(); + }); + + auto* dash = caelestia::config::GlobalConfig::instance()->dashboard(); + applyInterval(dash->resourceUpdateInterval()); + QObject::connect(dash, &caelestia::config::DashboardConfig::resourceUpdateIntervalChanged, this, [this, dash] { + applyInterval(dash->resourceUpdateInterval()); + }); +} + +int TickingService::updateInterval() const { + return m_interval; +} + +void TickingService::start() { + m_running = true; + if (m_interval > 0) { + m_timer->start(m_interval); + } + tick(); +} + +void TickingService::stop() { + m_running = false; + m_timer->stop(); +} + +void TickingService::applyInterval(int ms) { + if (ms <= 0 || ms == m_interval) { + return; + } + m_interval = ms; + if (m_running) { + m_timer->start(m_interval); + } + Q_EMIT updateIntervalChanged(); +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/tickingservice.hpp b/plugin/src/Caelestia/Services/tickingservice.hpp new file mode 100644 index 00000000..721b35d0 --- /dev/null +++ b/plugin/src/Caelestia/Services/tickingservice.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "service.hpp" + +namespace caelestia::services { + +class TickingService : public Service { + Q_OBJECT + + Q_PROPERTY(int updateInterval READ updateInterval NOTIFY updateIntervalChanged) + +public: + explicit TickingService(QObject* parent = nullptr); + + [[nodiscard]] int updateInterval() const; + +signals: + void updateIntervalChanged(); + +protected: + void start() final; + void stop() final; + + virtual void tick() = 0; + +private: + void applyInterval(int ms); + + QTimer* m_timer; + int m_interval = 1000; + bool m_running = false; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/usagefmt.cpp b/plugin/src/Caelestia/Services/usagefmt.cpp new file mode 100644 index 00000000..75ffe626 --- /dev/null +++ b/plugin/src/Caelestia/Services/usagefmt.cpp @@ -0,0 +1,33 @@ +#include "usagefmt.hpp" + +namespace { + +constexpr qreal kKib = 1024.0; +constexpr qreal kMib = kKib * 1024.0; +constexpr qreal kGib = kMib * 1024.0; + +bool finitePositive(qreal v) { + return std::isfinite(v) && v >= 0.0; +} + +} // namespace + +namespace caelestia::services::usagefmt { + +FormatResult UsageFmt::formatKib(qreal kib, qreal total) const { + if (!finitePositive(kib) || !finitePositive(total)) { + return { 0.0, 0.0, "KiB" }; + } + if (total >= kGib) { + return { kib / kGib, total / kGib, "TiB" }; + } + if (total >= kMib) { + return { kib / kMib, total / kMib, "GiB" }; + } + if (total >= kKib) { + return { kib / kKib, total / kKib, "MiB" }; + } + return { kib, total, "KiB" }; +} + +} // namespace caelestia::services::usagefmt diff --git a/plugin/src/Caelestia/Services/usagefmt.hpp b/plugin/src/Caelestia/Services/usagefmt.hpp new file mode 100644 index 00000000..2eee3c1c --- /dev/null +++ b/plugin/src/Caelestia/Services/usagefmt.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +namespace caelestia::services::usagefmt { + +struct FormatResult { + Q_GADGET + QML_ANONYMOUS + + Q_PROPERTY(qreal value MEMBER value CONSTANT) + Q_PROPERTY(qreal total MEMBER total CONSTANT) + Q_PROPERTY(QString unit MEMBER unit CONSTANT) + +public: + qreal value; + qreal total; + QString unit; +}; + +class UsageFmt : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + Q_INVOKABLE [[nodiscard]] FormatResult formatKib(qreal kib, qreal total) const; +}; + +} // namespace caelestia::services::usagefmt diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp index 28a0e178..b6080f6e 100644 --- a/plugin/src/Caelestia/cutils.cpp +++ b/plugin/src/Caelestia/cutils.cpp @@ -100,7 +100,7 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q }); } -bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const { +bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) { if (!source.isLocalFile()) { qCWarning(lcCUtils) << "copyFile: source" << source << "is not a local file"; return false; @@ -120,7 +120,7 @@ bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) co return QFile::copy(source.toLocalFile(), target.toLocalFile()); } -bool CUtils::deleteFile(const QUrl& path) const { +bool CUtils::deleteFile(const QUrl& path) { if (!path.isLocalFile()) { qCWarning(lcCUtils) << "deleteFile: path" << path << "is not a local file"; return false; @@ -129,7 +129,7 @@ bool CUtils::deleteFile(const QUrl& path) const { return QFile::remove(path.toLocalFile()); } -QString CUtils::toLocalFile(const QUrl& url) const { +QString CUtils::toLocalFile(const QUrl& url) { if (!url.isLocalFile()) { qCWarning(lcCUtils) << "toLocalFile: given url is not a local file" << url; return QString(); @@ -138,4 +138,20 @@ QString CUtils::toLocalFile(const QUrl& url) const { return url.toLocalFile(); } +qreal CUtils::clamp(qreal value, qreal min, qreal max) { + return qBound(min, value, max); +} + +#ifndef CAELESTIA_VERSION +#define CAELESTIA_VERSION "" +#endif + +QString CUtils::version() const { + return QStringLiteral(CAELESTIA_VERSION); +} + +QString CUtils::qtVersion() const { + return QStringLiteral(QT_VERSION_STR); +} + } // namespace caelestia diff --git a/plugin/src/Caelestia/cutils.hpp b/plugin/src/Caelestia/cutils.hpp index 027226d6..7fb4163a 100644 --- a/plugin/src/Caelestia/cutils.hpp +++ b/plugin/src/Caelestia/cutils.hpp @@ -11,6 +11,9 @@ class CUtils : public QObject { QML_ELEMENT QML_SINGLETON + Q_PROPERTY(QString version READ version CONSTANT) + Q_PROPERTY(QString qtVersion READ qtVersion CONSTANT) + public: // clang-format off Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path); @@ -21,9 +24,14 @@ public: Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed); // clang-format on - Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const; - Q_INVOKABLE bool deleteFile(const QUrl& path) const; - Q_INVOKABLE QString toLocalFile(const QUrl& url) const; + Q_INVOKABLE static bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true); + Q_INVOKABLE static bool deleteFile(const QUrl& path); + Q_INVOKABLE static QString toLocalFile(const QUrl& url); + + Q_INVOKABLE static qreal clamp(qreal value, qreal min, qreal max); + + [[nodiscard]] QString version() const; + [[nodiscard]] QString qtVersion() const; }; } // namespace caelestia diff --git a/services/Colours.qml b/services/Colours.qml index 922b51db..301f2e7f 100644 --- a/services/Colours.qml +++ b/services/Colours.qml @@ -25,6 +25,9 @@ Singleton { readonly property Transparency transparency: Transparency {} readonly property alias wallLuminance: analyser.luminance + property bool cooldownPending + property real lastBaseTransparency + function getLuminance(c: color): real { if (c.r == 0 && c.g == 0 && c.b == 0) return 0; @@ -84,7 +87,16 @@ Singleton { Hypr.extras.batchMessage([str.arg("blur").arg(transparency.enabled ? 1 : 0), str.arg("ignore_alpha").arg(transparency.base - 0.03)]); } - Component.onCompleted: debounceTimer.triggered() + function requestReloadHyprRules(): void { + if (cooldownTimer.running) { + root.cooldownPending = true; + } else { + root.reloadHyprRules(); + cooldownTimer.restart(); + } + } + + Component.onCompleted: root.requestReloadHyprRules() Connections { function onConfigReloaded(): void { @@ -108,10 +120,23 @@ Singleton { } Timer { - id: debounceTimer + id: cooldownTimer - interval: 300 - onTriggered: root.reloadHyprRules() + interval: 30 + onTriggered: { + if (root.cooldownPending) { + root.cooldownPending = false; + root.reloadHyprRules(); + restart(); + } + } + } + + Timer { + id: cAnimCompleteTimer + + interval: Tokens.anim.durations.expressiveSlowEffects + onTriggered: root.requestReloadHyprRules() } component Transparency: QtObject { @@ -119,8 +144,19 @@ Singleton { readonly property real base: Math.max(0, Math.min(1, Tokens.transparency.base - (root.light ? 0.1 : 0))) readonly property real layers: Tokens.transparency.layers - onEnabledChanged: debounceTimer.restart() - onBaseChanged: debounceTimer.restart() + onEnabledChanged: { + if (enabled) + root.requestReloadHyprRules(); + else + cAnimCompleteTimer.start(); + } + onBaseChanged: { + if (root.lastBaseTransparency > base) + root.requestReloadHyprRules(); + else + cAnimCompleteTimer.start(); + root.lastBaseTransparency = base; + } } component M3TPalette: QtObject { diff --git a/services/LyricsService.qml b/services/LyricsService.qml deleted file mode 100644 index 26a75e9b..00000000 --- a/services/LyricsService.qml +++ /dev/null @@ -1,403 +0,0 @@ -pragma Singleton - -import "../utils/scripts/lrcparser.js" as Lrc -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia -import Caelestia.Config -import qs.utils - -Singleton { - id: root - - property var player: Players.active - property int currentIndex: -1 - property bool loading: false - property bool isManualSeeking: false - property bool lyricsVisible: GlobalConfig.services.showLyrics - property string backend: "Local" - property string preferredBackend: GlobalConfig.services.lyricsBackend - property real currentSongId: 0 - property string loadedLocalFile: "" - property real offset - property int currentRequestId: 0 - property var lyricsMap: ({}) - - readonly property string lyricsDir: Paths.absolutePath(GlobalConfig.paths.lyricsDir) - readonly property string lyricsMapFile: lyricsDir + "/lyrics_map.json" - readonly property alias model: lyricsModel - readonly property alias candidatesModel: fetchedCandidatesModel - readonly property var _netEaseHeaders: ({ - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", - "Referer": "https://music.163.com/" - }) - - function getMetadata() { - if (!player || !player.metadata) - return null; - let artist = player.metadata["xesam:artist"]; - const title = player.metadata["xesam:title"]; - if (Array.isArray(artist)) - artist = artist.join(", "); - return { - artist: artist || "Unknown", - title: title || "Unknown" - }; - } - - function _metaKey(meta) { - return `${meta.artist} - ${meta.title}`; - } - - function savePrefs() { - let meta = getMetadata(); - if (!meta) - return; - let key = _metaKey(meta); - let existing = root.lyricsMap[key] ?? {}; - root.lyricsMap[key] = { - offset: root.offset, - backend: root.backend, - neteaseId: existing.neteaseId ?? null - }; - // reassign to notify QML bindings of the map change - root.lyricsMap = root.lyricsMap; - saveLyricsMap.command = ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap).replace(/'/g, "'\\''")}' > "${root.lyricsMapFile}"`]; - saveLyricsMap.running = true; - } - - function toggleVisibility() { - GlobalConfig.services.showLyrics = !GlobalConfig.services.showLyrics; - } - - function loadLyrics() { - loadDebounce.restart(); - } - - function _doLoadLyrics() { - const meta = getMetadata(); - if (!meta) { - lyricsModel.clear(); - root.currentIndex = -1; - return; - } - - loading = true; - lyricsModel.clear(); - currentIndex = -1; - root.currentSongId = 0; - - root.currentRequestId++; - let requestId = root.currentRequestId; - - let key = _metaKey(meta); - let saved = root.lyricsMap[key]; - root.offset = saved?.offset ?? 0.0; - - if (root.preferredBackend === "NetEase") { - root.backend = "NetEase"; - fetchNetEase(meta.title, meta.artist, requestId); - return; - } - - if (root.preferredBackend === "Local") { - root.backend = "Local"; - let cleanDir = lyricsDir.replace(/\/$/, ""); - let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; - - // Search for files matching "Artist - Title.lrc" pattern - const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); - const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); - const escapedTitle = titleStr.replace(/'/g, "'\\''"); - const escapedArtist = artistStr.replace(/'/g, "'\\''"); - findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; - findLyricsInSubdirs.requestId = requestId; - findLyricsInSubdirs.running = true; - - lrcFile.path = ""; - lrcFile.path = flatPath; - return; - } - - // Auto mode: try local first - root.backend = "Local"; - let cleanDir = lyricsDir.replace(/\/$/, ""); - let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; - - const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); - const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); - const escapedTitle = titleStr.replace(/'/g, "'\\''"); - const escapedArtist = artistStr.replace(/'/g, "'\\''"); - findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; - findLyricsInSubdirs.requestId = requestId; - findLyricsInSubdirs.running = true; - - lrcFile.path = ""; - lrcFile.path = flatPath; - fetchNetEaseCandidates(meta.title, meta.artist, requestId); - } - - function updateModel(parsedArray) { - root.currentIndex = -1; - lyricsModel.clear(); - for (let line of parsedArray) { - lyricsModel.append({ - time: line.time, - lyricLine: line.text - }); - } - } - - function fallbackToOnline() { - let meta = getMetadata(); - if (!meta) - return; - fetchNetEase(meta.title, meta.artist, root.currentRequestId); - } - - // NetEase - - // searches NetEase and populates the candidates model. returns the result array via the onResults callback - function _searchNetEase(title, artist, reqId, onResults) { - Requests.resetCookies(); - const query = encodeURIComponent(`${title} ${artist}`); - const url = `https://music.163.com/api/search/get?s=${query}&type=1&limit=5`; - - Requests.get(url, text => { - if (reqId !== root.currentRequestId) - return; - const res = JSON.parse(text); - const songs = res.result?.songs || []; - - fetchedCandidatesModel.clear(); - for (let s of songs) { - fetchedCandidatesModel.append({ - id: s.id, - title: s.name || "Unknown Title", - artist: s.artists?.map(a => a.name).join(", ") || "Unknown Artist" - }); - } - - onResults(songs); - }, err => {}, root._netEaseHeaders); - } - - // populates the candidates model only. used when a saved NetEase ID already exists and we just want to refresh the picker list. - function fetchNetEaseCandidates(title, artist, reqId) { - _searchNetEase(title, artist, reqId, _songs => {}); - } - - // searches NetEase, populates candidates, then auto-selects the best match and fetches its lyrics. - function fetchNetEase(title, artist, reqId) { - _searchNetEase(title, artist, reqId, songs => { - const bestMatch = songs.find(s => { - const inputArtist = String(artist || "").toLowerCase(); - const sArtist = String(s.artists?.[0]?.name || "").toLowerCase(); - return inputArtist.includes(sArtist) || sArtist.includes(inputArtist); - }); - - if (!bestMatch) { - return; // No reliable lyrics found - } - - let key = `${artist} - ${title}`; - root.lyricsMap[key] = { - offset: root.lyricsMap[key]?.offset ?? 0.0, - backend: "NetEase", - neteaseId: bestMatch.id - }; - root.currentSongId = bestMatch.id; - savePrefs(); - fetchNetEaseLyrics(bestMatch.id, reqId); - }); - } - - function fetchNetEaseLyrics(id, reqId) { - const url = `https://music.163.com/api/song/lyric?id=${id}&lv=1&kv=1&tv=-1`; - Requests.get(url, text => { - if (reqId !== root.currentRequestId) - return; - const res = JSON.parse(text); - if (res.lrc?.lyric) { - updateModel(Lrc.parseLrc(res.lrc.lyric)); - loading = false; - } - }); - } - - function selectCandidate(songId) { - let meta = getMetadata(); - if (!meta) - return; - root.backend = "NetEase"; - root.currentSongId = songId; - let key = _metaKey(meta); - root.lyricsMap[key] = { - offset: root.lyricsMap[key]?.offset ?? 0.0, - neteaseId: songId - }; - savePrefs(); - fetchNetEaseLyrics(songId, currentRequestId); - } - - function updatePosition() { - if (isManualSeeking || loading || !player || lyricsModel.count === 0) - return; - - let pos = player.position - root.offset; - let newIdx = -1; - for (let i = lyricsModel.count - 1; i >= 0; i--) { - if (pos >= lyricsModel.get(i).time - 0.1) { // 100ms fudge factor - newIdx = i; - break; - } - } - - if (newIdx !== currentIndex) { - root.currentIndex = newIdx; - } - } - - function jumpTo(index, time) { - root.isManualSeeking = true; - root.currentIndex = index; - - if (player) { - player.position = time + root.offset + 0.01; // compensate for rounding - } - - seekTimer.restart(); - } - - onPreferredBackendChanged: { - if (GlobalConfig.services.lyricsBackend !== preferredBackend) { - GlobalConfig.services.lyricsBackend = preferredBackend; - } - } - - ListModel { - id: lyricsModel - } - - ListModel { - id: fetchedCandidatesModel - } - - Timer { - id: seekTimer - - interval: 500 - onTriggered: root.isManualSeeking = false - } - - // If no local lyrics were loaded within the interval, fall back to NetEase - Timer { - id: fallbackTimer - - interval: 200 - onTriggered: { - if (lyricsModel.count === 0) { - root.backend = "NetEase"; - fallbackToOnline(); - } - } - } - - Timer { - id: loadDebounce - - interval: 50 - onTriggered: root._doLoadLyrics() - } - - FileView { - id: lyricsMapFileView - - path: root.lyricsMapFile - printErrors: false - onLoaded: { - try { - root.lyricsMap = JSON.parse(text()); - } catch (e) { - root.lyricsMap = {}; - } - } - } - - FileView { - id: lrcFile - - printErrors: false - onLoaded: { - fallbackTimer.stop(); - let parsed = Lrc.parseLrc(text()); - if (parsed.length > 0) { - root.backend = "Local"; - root.loadedLocalFile = path; - updateModel(parsed); - loading = false; - } else if (root.preferredBackend === "Local") { - // Local mode only - fail immediately - root.backend = "NetEase"; - fallbackToOnline(); - } - // In Auto mode, let the Process onExited handle fallback - } - } - - Connections { - function onActiveChanged() { - root.player = Players.active; - loadLyrics(); - } - - target: Players - } - - Connections { - function onMetadataChanged() { - loadLyrics(); - } - - target: root.player - ignoreUnknownSignals: true - } - - Process { - id: saveLyricsMap - - command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] - } - - Process { - id: findLyricsInSubdirs - - property int requestId: -1 - property bool foundFile: false - - stdout: SplitParser { - onRead: data => { - if (findLyricsInSubdirs.requestId === root.currentRequestId) { - const foundPath = data.trim(); - if (foundPath && foundPath.length > 0) { - findLyricsInSubdirs.foundFile = true; - fallbackTimer.stop(); - root.loadedLocalFile = foundPath; - lrcFile.path = ""; - lrcFile.path = foundPath; - } - } - } - } - - onExited: (exitCode, exitStatus) => { // qmllint disable signal-handler-parameters - if (requestId === root.currentRequestId && !foundFile && root.preferredBackend === "Auto") { - if (lyricsModel.count === 0) { - fallbackTimer.restart(); - } - } - foundFile = false; - } - } -} diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index 6c4dc8d2..c3299a12 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -20,8 +20,8 @@ Singleton { readonly property real uploadTotal: _uploadTotal // History buffers for sparkline - readonly property CircularBuffer downloadBuffer: _downloadBuffer - readonly property CircularBuffer uploadBuffer: _uploadBuffer + readonly property alias downloadBuffer: downloadHistory + readonly property alias uploadBuffer: uploadHistory readonly property int historyLength: 30 // Private properties @@ -137,13 +137,13 @@ Singleton { } CircularBuffer { - id: _downloadBuffer + id: downloadHistory capacity: root.historyLength + 1 } CircularBuffer { - id: _uploadBuffer + id: uploadHistory capacity: root.historyLength + 1 } @@ -200,10 +200,10 @@ Singleton { root._uploadSpeed = txDelta / timeDelta; if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) - _downloadBuffer.push(root._downloadSpeed); + downloadHistory.push(root._downloadSpeed); if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) - _uploadBuffer.push(root._uploadSpeed); + uploadHistory.push(root._uploadSpeed); } // Calculate totals with overflow handling diff --git a/services/Nmcli.qml b/services/Nmcli.qml index ea5d7eb3..e9827149 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -12,6 +12,7 @@ Singleton { property var wirelessInterfaces: [] property var ethernetInterfaces: [] property bool isConnected: false + readonly property bool connecting: wirelessInterfaces.some(i => isConnectingState(i.state)) property string activeInterface: "" property string activeConnection: "" property bool wifiEnabled: true @@ -166,6 +167,15 @@ Singleton { return state === "100 (connected)" || state === "connected" || state.startsWith("connected"); } + function isConnectingState(state: string): bool { + return !!state && state.startsWith("connecting"); + } + + function connectingSsid(): string { + const iface = root.wirelessInterfaces.find(i => isConnectingState(i.state)); + return iface ? iface.connection : ""; + } + function executeCommand(args: list, callback: var): void { const proc = commandProc.createObject(root); proc.cmdArgs = ["nmcli", ...args]; diff --git a/services/Players.qml b/services/Players.qml index 41507839..59f96214 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -16,6 +16,8 @@ Singleton { property alias manualActive: props.manualActive function getIdentity(player: MprisPlayer): string { + if (!player) + return ""; const alias = GlobalConfig.services.playerAliases.find(a => a.from === player.identity); return alias?.to ?? player.identity; } diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml deleted file mode 100644 index 15dda611..00000000 --- a/services/SystemUsage.qml +++ /dev/null @@ -1,330 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia.Config - -Singleton { - id: root - - // CPU properties - property string cpuName: "" - property real cpuPerc - property real cpuTemp - - // GPU properties - readonly property string gpuType: GlobalConfig.services.gpuType.toUpperCase() || autoGpuType - property string autoGpuType: "NONE" - property string gpuName: "" - property real gpuPerc - property real gpuTemp - - // Memory properties - property real memUsed - property real memTotal - readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0 - - // Storage properties (aggregated) - readonly property real storagePerc: { - let totalUsed = 0; - let totalSize = 0; - for (const disk of disks) { - totalUsed += disk.used; - totalSize += disk.total; - } - return totalSize > 0 ? totalUsed / totalSize : 0; - } - - // Individual disks: Array of { mount, used, total, free, perc } - property var disks: [] - - property real lastCpuIdle - property real lastCpuTotal - - property int refCount - - function cleanCpuName(name: string): string { - return name.replace(/\(R\)|\(TM\)|CPU|\d+(?:th|nd|rd|st) Gen |Core |Processor/gi, "").replace(/\s+/g, " ").trim(); - } - - function cleanGpuName(name: string): string { - return name.replace(/\(R\)|\(TM\)|Graphics/gi, "").replace(/\s+/g, " ").trim(); - } - - function formatKib(kib: real): var { - const mib = 1024; - const gib = 1024 ** 2; - const tib = 1024 ** 3; - - if (kib >= tib) - return { - value: kib / tib, - unit: "TiB" - }; - if (kib >= gib) - return { - value: kib / gib, - unit: "GiB" - }; - if (kib >= mib) - return { - value: kib / mib, - unit: "MiB" - }; - return { - value: kib, - unit: "KiB" - }; - } - - Timer { - running: root.refCount > 0 - interval: GlobalConfig.dashboard.resourceUpdateInterval - repeat: true - triggeredOnStart: true - onTriggered: { - stat.reload(); - meminfo.reload(); - storage.running = true; - gpuUsage.running = true; - sensors.running = true; - } - } - - // One-time CPU info detection (name) - FileView { - id: cpuinfoInit - - path: "/proc/cpuinfo" - onLoaded: { - const nameMatch = text().match(/model name\s*:\s*(.+)/); - if (nameMatch) - root.cpuName = root.cleanCpuName(nameMatch[1]); - } - } - - FileView { - id: stat - - path: "/proc/stat" - onLoaded: { - const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); - if (data) { - const stats = data.slice(1).map(n => parseInt(n, 10)); - const total = stats.reduce((a, b) => a + b, 0); - const idle = stats[3] + (stats[4] ?? 0); - - const totalDiff = total - root.lastCpuTotal; - const idleDiff = idle - root.lastCpuIdle; - root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; - - root.lastCpuTotal = total; - root.lastCpuIdle = idle; - } - } - } - - FileView { - id: meminfo - - path: "/proc/meminfo" - onLoaded: { - const data = text(); - root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1; - root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0; - } - } - - Process { - id: storage - - // Get physical disks with aggregated usage from their partitions - // -J triggers JSON output. -b triggers bytes. - command: ["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT"] - - stdout: StdioCollector { - onStreamFinished: { - const data = JSON.parse(text); - const diskList = []; - const seenDevices = new Set(); - - // Helper to recursively sum usage from children (partitions, crypt, lvm) - const aggregateUsage = dev => { - let used = 0; - let size = 0; - let isRoot = dev.mountpoint === "/" || (dev.mountpoints && dev.mountpoints.includes("/")); - - if (!seenDevices.has(dev.name)) { - // lsblk returns null for empty/unformatted partitions, which parses to 0 here - used = parseInt(dev.fsused) || 0; - size = parseInt(dev.fssize) || 0; - seenDevices.add(dev.name); - } - - if (dev.children) { - for (const child of dev.children) { - const stats = aggregateUsage(child); - used += stats.used; - size += stats.size; - if (stats.isRoot) - isRoot = true; - } - } - return { - used, - size, - isRoot - }; - }; - - for (const dev of data.blockdevices) { - // Only process physical disks at the top level - if (dev.type === "disk" && !dev.name.startsWith("zram")) { - const stats = aggregateUsage(dev); - - if (stats.size === 0) { - continue; - } - - const total = stats.size; - const used = stats.used; - - diskList.push({ - mount: dev.name, - used: used / 1024 // KiB - , - total: total / 1024 // KiB - , - free: (total - used) / 1024, - perc: total > 0 ? used / total : 0, - hasRoot: stats.isRoot - }); - } - } - - // Sort by putting the disk with root first, then sort the rest alphabetically - root.disks = diskList.sort((a, b) => { - if (a.hasRoot && !b.hasRoot) - return -1; - if (!a.hasRoot && b.hasRoot) - return 1; - return a.mount.localeCompare(b.mount); - }); - } - } - } - - // GPU name detection (one-time) - Process { - id: gpuNameDetect - - running: true - command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1 || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1"] - stdout: StdioCollector { - onStreamFinished: { - const output = text.trim(); - if (!output) - return; - - // Check if it's from nvidia-smi (clean GPU name) - if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) { - root.gpuName = root.cleanGpuName(output); - } else if (output.toLowerCase().includes("rx")) { - root.gpuName = root.cleanGpuName(output); - } else { - // Parse lspci output: extract name from brackets or after colon - // Handles cases like [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] (rev c0) - const bracketMatch = output.match(/\[([^\]]+)\][^\[]*$/); - if (bracketMatch) { - root.gpuName = root.cleanGpuName(bracketMatch[1]); - } else { - const colonMatch = output.match(/:\s*(.+)/); - if (colonMatch) - root.gpuName = root.cleanGpuName(colonMatch[1]); - } - } - } - } - } - - Process { - id: gpuTypeCheck - - running: !GlobalConfig.services.gpuType - command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] - stdout: StdioCollector { - onStreamFinished: root.autoGpuType = text.trim() - } - } - - Process { - id: gpuUsage - - command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu", "--format=csv,noheader,nounits"] : ["echo"] - stdout: StdioCollector { - onStreamFinished: { - if (root.gpuType === "GENERIC") { - const percs = text.trim().split("\n"); - const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); - root.gpuPerc = sum / percs.length / 100; - } else if (root.gpuType === "NVIDIA") { - const [usage, temp] = text.trim().split(","); - root.gpuPerc = parseInt(usage, 10) / 100; - root.gpuTemp = parseInt(temp, 10); - } else { - root.gpuPerc = 0; - root.gpuTemp = 0; - } - } - } - } - - Process { - id: sensors - - command: ["sensors"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/); - if (!cpuTemp) - // If AMD Tdie pattern failed, try fallback on Tctl - cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/); - - if (cpuTemp) - root.cpuTemp = parseFloat(cpuTemp[1]); - - if (root.gpuType !== "GENERIC") - return; - - let eligible = false; - let sum = 0; - let count = 0; - - for (const line of text.trim().split("\n")) { - if (line === "Adapter: PCI adapter") - eligible = true; - else if (line === "") - eligible = false; - else if (eligible) { - let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/); - if (!match) - // Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs) - match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/); - - if (match) { - sum += parseFloat(match[2]); - count++; - } - } - } - - root.gpuTemp = count > 0 ? sum / count : 0; - } - } - } -} diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index 15daf5c1..a56ff348 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -20,6 +20,17 @@ Searcher { property string actualCurrent property bool previewColourLock + function getCategoryFor(w: FileSystemEntry): string { + let category = w.parentDir.slice(Paths.wallsdir.length + 1); + if (category.includes("/")) + category = category.slice(0, category.indexOf("/")); + return category; + } + + function setRandom(): void { + Quickshell.execDetached(["caelestia", "wallpaper", "-r", ...smartArg]); + } + function setWallpaper(path: string): void { actualCurrent = path; Quickshell.execDetached(["caelestia", "wallpaper", "-f", path, ...smartArg]); diff --git a/services/Weather.qml b/services/Weather.qml index 9c30bf7a..f2b36eec 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -17,8 +17,8 @@ Singleton { readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" readonly property string description: cc?.weatherDesc ?? qsTr("No weather") - readonly property string temp: GlobalConfig.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C` - readonly property string feelsLike: GlobalConfig.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C` + readonly property string temp: formatTemp(cc?.tempC) + readonly property string feelsLike: formatTemp(cc?.feelsLikeC) readonly property int humidity: cc?.humidity ?? 0 readonly property real windSpeed: cc?.windSpeed ?? 0 readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), GlobalConfig.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" @@ -26,6 +26,10 @@ Singleton { readonly property var cachedCities: new Map() + function formatTemp(temp: var): string { + return GlobalConfig.services.useFahrenheit ? `${temp !== undefined ? Math.round(toFahrenheit(temp)) : "--"}°F` : `${temp !== undefined ? Math.round(temp) : "--"}°C`; + } + function reload(): void { const configLocation = GlobalConfig.services.weatherLocation; @@ -114,10 +118,8 @@ Singleton { cc = { weatherCode: json.current.weather_code, weatherDesc: getWeatherCondition(json.current.weather_code), - tempC: Math.round(json.current.temperature_2m), - tempF: Math.round(toFahrenheit(json.current.temperature_2m)), - feelsLikeC: Math.round(json.current.apparent_temperature), - feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)), + tempC: json.current.temperature_2m, + feelsLikeC: json.current.apparent_temperature, humidity: json.current.relative_humidity_2m, windSpeed: json.current.wind_speed_10m, isDay: json.current.is_day, @@ -129,10 +131,8 @@ Singleton { for (let i = 0; i < json.daily.time.length; i++) forecastList.push({ date: json.daily.time[i].replace(/-/g, "/"), - maxTempC: Math.round(json.daily.temperature_2m_max[i]), - maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])), - minTempC: Math.round(json.daily.temperature_2m_min[i]), - minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])), + maxTempC: json.daily.temperature_2m_max[i], + minTempC: json.daily.temperature_2m_min[i], weatherCode: json.daily.weather_code[i], icon: Icons.getWeatherIcon(json.daily.weather_code[i]) }); @@ -150,7 +150,7 @@ Singleton { timestamp: json.hourly.time[i], hour: time.getHours(), tempC: Math.round(json.hourly.temperature_2m[i]), - tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])), + precipChance: json.hourly.precipitation_probability[i], weatherCode: json.hourly.weather_code[i], icon: Icons.getWeatherIcon(json.hourly.weather_code[i]) }); @@ -169,7 +169,7 @@ Singleton { const [lat, lon] = loc.split(",").map(s => s.trim()); const baseUrl = "https://api.open-meteo.com/v1/forecast"; - const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"]; + const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m,precipitation_probability", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"]; return baseUrl + "?" + params.join("&"); } diff --git a/shell.qml b/shell.qml index bc2bb164..273d541c 100644 --- a/shell.qml +++ b/shell.qml @@ -14,6 +14,8 @@ import Quickshell ShellRoot { settings.watchFiles: true + GSFLoader {} + Background {} Drawers {} AreaPicker {} diff --git a/utils/Icons.qml b/utils/Icons.qml index c864d553..5bbbff57 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -241,23 +241,12 @@ Singleton { return icon; } - function getBatteryIcon(charge: int): string { - if (charge > 0 && charge < 5) - return "battery_0_bar"; - if (charge >= 5 && charge < 20) - return "battery_1_bar"; - if (charge >= 20 && charge < 35) - return "battery_2_bar"; - if (charge >= 35 && charge < 50) - return "battery_3_bar"; - if (charge >= 50 && charge < 65) - return "battery_4_bar"; - if (charge >= 65 && charge < 80) - return "battery_5_bar"; - if (charge >= 80 && charge < 95) - return "battery_6_bar"; - if (charge >= 95) - return "battery_full"; - return "battery_alert"; + function getBatteryIcon(percentage: real, charging = false): string { + if (percentage === 1) + return charging ? "battery_charging_full" : "battery_full"; + let level = Math.floor(percentage * 7); + if (charging && (level === 4 || level === 1)) + level--; + return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; } } diff --git a/utils/Searcher.qml b/utils/Searcher.qml index 102c9e76..4a1c1f85 100644 --- a/utils/Searcher.qml +++ b/utils/Searcher.qml @@ -35,7 +35,7 @@ Singleton { } function query(search: string): list { - search = transformSearch(search); + search = transformSearch(search.trim().replace(/\s+/g, " ")); if (!search) return [...list]; diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml index c715b8d2..c18ea560 100644 --- a/utils/SysInfo.qml +++ b/utils/SysInfo.qml @@ -21,6 +21,28 @@ Singleton { readonly property string wm: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP") readonly property string shell: Quickshell.env("SHELL").split("/").pop() + property string kernel + property string hostname + property string firmware + + // DMI vendor/model, combined into a single human-readable device name + property string boardVendor + property string boardName + readonly property string device: { + if (!boardName) + return boardVendor; + if (!boardVendor || boardName.toLowerCase().startsWith(boardVendor.toLowerCase())) + return boardName; + return `${boardVendor} ${boardName}`; + } + + // Strips the placeholder strings OEMs commonly leave in DMI fields + function sanitiseDmi(s: string): string { + const t = s.trim(); + const junk = ["to be filled by o.e.m.", "system product name", "system manufacturer", "system version", "default string", "o.e.m.", "not specified", "not applicable", "unknown", "none", ""]; + return junk.includes(t.toLowerCase()) ? "" : t; + } + FileView { id: osRelease @@ -57,6 +79,34 @@ Singleton { target: GlobalConfig.general } + FileView { + path: "/proc/sys/kernel/osrelease" + onLoaded: root.kernel = text().trim() + } + + FileView { + path: "/proc/sys/kernel/hostname" + onLoaded: root.hostname = text().trim() + } + + FileView { + path: "/sys/class/dmi/id/sys_vendor" + printErrors: false + onLoaded: root.boardVendor = root.sanitiseDmi(text()) + } + + FileView { + path: "/sys/class/dmi/id/product_name" + printErrors: false + onLoaded: root.boardName = root.sanitiseDmi(text()) + } + + FileView { + path: "/sys/class/dmi/id/bios_version" + printErrors: false + onLoaded: root.firmware = root.sanitiseDmi(text()) + } + Timer { running: true repeat: true diff --git a/utils/scripts/lrcparser.js b/utils/scripts/lrcparser.js deleted file mode 100644 index 847779ed..00000000 --- a/utils/scripts/lrcparser.js +++ /dev/null @@ -1,62 +0,0 @@ -function parseLrc(text) { - if (!text) return []; - let lines = text.split("\n"); - let result = []; - - let timeRegex = /\[(\d+):(\d+\.\d+|\d+)\]/g; - - // Blacklist for credits/metadata often found in NetEase lyrics - const creditKeywords = [ - "作词", "作曲", "编曲", "制作", "收录", "演奏", "词:", "曲:", "Lyricist", "Composer", "Arranger", "Producer", "Mixing", "Mastering" - ]; - - for (let line of lines) { - - timeRegex.lastIndex = 0; - let matches = []; - let match; - - while ((match = timeRegex.exec(line)) !== null) { - matches.push(match); - } - - if (matches.length === 0) continue; - - let lyric = line.replace(timeRegex, "").trim(); - - let min = parseInt(matches[0][1]); - let sec = parseFloat(matches[0][2]); - let totalTime = min * 60 + sec; - - // Only filter credits if they appear in the first 20 seconds - if (totalTime < 20) { - let isCreditFormat = creditKeywords.some(k => lyric.includes(k)); - if (isCreditFormat && (lyric.includes(":") || lyric.includes(":") || lyric.length < 25)) { - continue; - } - } - - for (let match of matches) { - let min = parseInt(match[1]); - let sec = parseFloat(match[2]); - - result.push({ - time: min * 60 + sec, - text: lyric - }); - } - } - - result.sort((a, b) => a.time - b.time); - return result; -} - -function getCurrentLine(lyrics, position) { - const epsilon = 0.1; // 100ms tolerance - for (let i = lyrics.length - 1; i >= 0; i--) { - if ((position + epsilon) >= lyrics[i].time) { - return i; - } - } - return -1; -}