quickshell: should be everything I want now
This commit is contained in:
21
.licenses/noctalia-dev/noctalia-shell
Normal file
21
.licenses/noctalia-dev/noctalia-shell
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 noctalia-dev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -34,6 +34,8 @@
|
|||||||
- fuzzel: edit $HOME/.config/fuzzel/fuzzel.ini
|
- fuzzel: edit $HOME/.config/fuzzel/fuzzel.ini
|
||||||
|
|
||||||
- niri: edit $HOME/.config/niri/config.kdl
|
- niri: edit $HOME/.config/niri/config.kdl
|
||||||
|
|
||||||
|
- quickshell qs ipc call colors setPrimary $hex
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -216,6 +218,11 @@ def _change_niri(palette: dict[str, str], flavor: str):
|
|||||||
replace_placeholders(niri_dist, palette, flavor)
|
replace_placeholders(niri_dist, palette, flavor)
|
||||||
|
|
||||||
|
|
||||||
|
def _change_quickshell(palette: dict[str, str], flavor: str):
|
||||||
|
hex_color = palette[flavor]
|
||||||
|
os.system(f'qs ipc call colors setPrimary {hex_color}')
|
||||||
|
|
||||||
|
|
||||||
apply_theme_funcs: dict[str, Callable[[dict[str, str], str], None]] = {
|
apply_theme_funcs: dict[str, Callable[[dict[str, str], str], None]] = {
|
||||||
'kvantum': _change_kvantum,
|
'kvantum': _change_kvantum,
|
||||||
# 'nwg-look': _change_nwglook,
|
# 'nwg-look': _change_nwglook,
|
||||||
@@ -230,6 +237,7 @@ apply_theme_funcs: dict[str, Callable[[dict[str, str], str], None]] = {
|
|||||||
'wlogout': _change_wlogout,
|
'wlogout': _change_wlogout,
|
||||||
'fuzzel': _change_fuzzel,
|
'fuzzel': _change_fuzzel,
|
||||||
'niri': _change_niri,
|
'niri': _change_niri,
|
||||||
|
'quickshell': _change_quickshell,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -333,7 +341,8 @@ def main():
|
|||||||
parser.add_argument('-i', '--image', type=str, help="Path to the image")
|
parser.add_argument('-i', '--image', type=str, help="Path to the image")
|
||||||
parser.add_argument('-f', '--flavor', type=str, help="Flavor to apply")
|
parser.add_argument('-f', '--flavor', type=str, help="Flavor to apply")
|
||||||
parser.add_argument('-c', '--color', type=str, help="Color to match from the palette")
|
parser.add_argument('-c', '--color', type=str, help="Color to match from the palette")
|
||||||
parser.add_argument('arguments', nargs='*', help="List of applications to change the color theme of")
|
parser.add_argument('arguments', nargs='*',
|
||||||
|
help="List of applications to change the color theme of, or !app to exclude an application. Available apps: " + ', '.join(apply_theme_funcs.keys()))
|
||||||
|
|
||||||
arguments = parser.parse_args()
|
arguments = parser.parse_args()
|
||||||
|
|
||||||
@@ -363,15 +372,34 @@ def main():
|
|||||||
return flavor
|
return flavor
|
||||||
|
|
||||||
def parse_apps() -> list[str]:
|
def parse_apps() -> list[str]:
|
||||||
|
apps = set()
|
||||||
if not arguments.arguments:
|
if not arguments.arguments:
|
||||||
return list(apply_theme_funcs.keys())
|
apps = set(apply_theme_funcs.keys())
|
||||||
apps = []
|
else:
|
||||||
|
allExclude = True
|
||||||
|
for arg in arguments.arguments:
|
||||||
|
if arg[0] == '!':
|
||||||
|
continue
|
||||||
|
allExclude = False
|
||||||
|
if arg not in apply_theme_funcs:
|
||||||
|
print(f"Unknown app: {arg}. Available apps: {', '.join(apply_theme_funcs.keys())}")
|
||||||
|
sys.exit(1)
|
||||||
|
apps.add(arg)
|
||||||
|
|
||||||
|
# If all arguments are exclusions, start with all apps
|
||||||
|
if allExclude:
|
||||||
|
apps = set(apply_theme_funcs.keys())
|
||||||
|
|
||||||
for arg in arguments.arguments:
|
for arg in arguments.arguments:
|
||||||
if arg not in apply_theme_funcs:
|
if arg[0] == '!':
|
||||||
print(f"Unknown app: {arg}. Available apps: {', '.join(apply_theme_funcs.keys())}")
|
print(f"Excluding app: {arg[1:]}")
|
||||||
sys.exit(1)
|
app = arg[1:]
|
||||||
apps.append(arg)
|
if app not in apply_theme_funcs:
|
||||||
return apps
|
print(f"Unknown app to exclude: {app}. Available apps: {', '.join(apply_theme_funcs.keys())}")
|
||||||
|
sys.exit(1)
|
||||||
|
apps.discard(app)
|
||||||
|
|
||||||
|
return list(apps)
|
||||||
|
|
||||||
palette_name = parse_palette_name()
|
palette_name = parse_palette_name()
|
||||||
palette = PALETTES[palette_name]
|
palette = PALETTES[palette_name]
|
||||||
|
|||||||
@@ -80,13 +80,13 @@ if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
|
|||||||
|
|
||||||
notify-send "Wallpaper Changed" "$image"
|
notify-send "Wallpaper Changed" "$image"
|
||||||
|
|
||||||
change-colortheme -i "$image_copied" || exit 1
|
change-colortheme -i "$image_copied" !quickshell || exit 1
|
||||||
elif [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
|
elif [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
|
||||||
swww img -n background "$image_copied" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
|
swww img -n background "$image_copied" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
|
||||||
|
|
||||||
notify-send "Wallpaper Changed" "$image"
|
notify-send "Wallpaper Changed" "$image"
|
||||||
|
|
||||||
change-colortheme -i "$image_copied" || exit 1
|
change-colortheme -i "$image_copied" !waybar !eww || exit 1
|
||||||
else
|
else
|
||||||
echo "Unsupported desktop environment: $XDG_CURRENT_DESKTOP"
|
echo "Unsupported desktop environment: $XDG_CURRENT_DESKTOP"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ else
|
|||||||
desktop="$1"
|
desktop="$1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for item in "waybar" "kitty" "ghostty" "wlogout"; do
|
for item in "kitty" "ghostty" "wlogout"; do
|
||||||
[ -L "$HOME/.config/$item" ] || exit 1
|
[ -L "$HOME/.config/$item" ] || exit 1
|
||||||
|
|
||||||
rm "$HOME/.config/$item"
|
rm "$HOME/.config/$item"
|
||||||
|
|||||||
4
.scripts/sl-wrap
Executable file
4
.scripts/sl-wrap
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
pgrep -f spotify-lyrics && (killall spotify-lyrics || exit 1)
|
||||||
|
spotify-lyrics "$@"
|
||||||
36
README.md
36
README.md
@@ -1,35 +1,43 @@
|
|||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
- desktop with a few widgets:
|
- Hyprland & Waybar & Eww:
|
||||||
<img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/desktop.jpg?raw=true"/>
|
|
||||||
|
|
||||||
- dynamic colortheme based on Catppuccin Mocha:
|
<img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/desktop.jpg?raw=true"/>
|
||||||
<img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/desktop-alt.jpg?raw=true"/>
|
|
||||||
|
|
||||||
- the grub menu looks like:
|
- Niri & Quickshell
|
||||||
|
|
||||||
|
<img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/desktop-alt.jpg?raw=true"/>
|
||||||
|
<img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/backdrop.jpg?raw=true"/>
|
||||||
|
|
||||||
|
- Grub menu:
|
||||||
<img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/grub.jpg?raw=true"/>
|
<img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/grub.jpg?raw=true"/>
|
||||||
|
|
||||||
## Setup Overview
|
## Setup Overview
|
||||||
|
|
||||||
- **OS**: Archlinux
|
- **OS**: Archlinux
|
||||||
- **WM**: Niri & Hyprland (looks similar through screenshots)
|
- **WM**: Hyprland | Niri
|
||||||
- **Bar**: Waybar
|
- **Bar**: Waybar | Quickshell
|
||||||
- **Shell**: Fish
|
- **Shell**: Fish
|
||||||
- **Prompt**: Oh My Posh
|
- **Prompt**: Oh My Posh
|
||||||
- **Terminal**: Kitty & Ghostty
|
- **Terminal**: Kitty & Ghostty
|
||||||
- **Colorscheme**: Catppuccin Mocha
|
- **Colorscheme**: Catppuccin Mocha
|
||||||
- **App Launcher**: Rofi
|
- **App Launcher**: Rofi
|
||||||
- **Logout Screen**: Wlogout
|
- **Logout Screen**: Wlogout
|
||||||
- **Desktop Widgets**: Eww
|
- **Desktop Widgets**: Eww | Quickshell
|
||||||
|
- **Wallpaper Darmon**: swww
|
||||||
- **Notification Daemon**: Mako
|
- **Notification Daemon**: Mako
|
||||||
|
|
||||||
## Hyprland & friends
|
## Hyprland & friends
|
||||||
|
|
||||||
Based on [end-4/dots-hyprland](https://github.com/end-4/dots-hyprland) but without ags amd tons of other stuff.
|
Based on an old version of [end-4/dots-hyprland](https://github.com/end-4/dots-hyprland) but without ags amd tons of other stuff.
|
||||||
|
|
||||||
## Niri
|
## Niri
|
||||||
|
|
||||||
Ported from Hyprland, and shares most of the desktop components such as hyprlock & eww widgets & rofi & waybar & mako.
|
Ported from Hyprland, and shares some of the desktop components such as hyprlock & hypridle & mako, but uses quickshell as bar and desktop-widgets instead of the combination of waybar and eww.
|
||||||
|
|
||||||
|
## Quickshell
|
||||||
|
|
||||||
|
Not based on, but heaviely depends on modules from [noctalia-shell](https://github.com/noctalia-dev/noctalia-shell). This setup is currently only adapted for Niri.
|
||||||
|
|
||||||
## Eww
|
## Eww
|
||||||
|
|
||||||
@@ -37,13 +45,17 @@ Ported from Hyprland, and shares most of the desktop components such as hyprlock
|
|||||||
- `lyrics`, scrolling lyrics player, depends on [a small program](https://github.com/Uyanide/spotify-lyrics) from myself <small>(which also happens to be my frist Golang program :D)</small>.
|
- `lyrics`, scrolling lyrics player, depends on [a small program](https://github.com/Uyanide/spotify-lyrics) from myself <small>(which also happens to be my frist Golang program :D)</small>.
|
||||||
- `lyrics-single`, similar to `lyrics`, but only with a single line and can be easily embeded into the status bar.
|
- `lyrics-single`, similar to `lyrics`, but only with a single line and can be easily embeded into the status bar.
|
||||||
|
|
||||||
|
# SWWW
|
||||||
|
|
||||||
|
In Niri, the wallpaper will be automatically blurred when there are windows in focus. And the backdrop also has a blurred wallpaper applied to it. These are implemented in [wallpaper-daemon](https://github.com/Uyanide/dotfiles/blob/main/.scripts/wallpaper-daemon).
|
||||||
|
|
||||||
## Rofi
|
## Rofi
|
||||||
|
|
||||||
Based on [codeopshq/dotfiles](https://github.com/codeopshq/dotfiles), also serves as cliphist browser and emojis picker.
|
Based on [codeopshq/dotfiles](https://github.com/codeopshq/dotfiles), also serves as cliphist browser and emojis picker.
|
||||||
|
|
||||||
## Grub theme
|
## Grub theme
|
||||||
|
|
||||||
Based on [vinceliuice/Elegant-grub2-themes](https://github.com/vinceliuice/Elegant-grub2-themes) with [illustration from 紺屋鴉江](https://www.pixiv.net/artworks/119683453).
|
Based on [vinceliuice/Elegant-grub2-themes](https://github.com/vinceliuice/Elegant-grub2-themes) with the [illustration from 紺屋鴉江](https://www.pixiv.net/artworks/119683453).
|
||||||
|
|
||||||
## MPV
|
## MPV
|
||||||
|
|
||||||
@@ -57,8 +69,8 @@ See [backgrounds repo for personal usage](https://github.com/Uyanide/backgrounds
|
|||||||
|
|
||||||
including:
|
including:
|
||||||
|
|
||||||
- MesloLGM Nerd Font (& Mono)
|
|
||||||
- Maple Mono NF CN
|
- Maple Mono NF CN
|
||||||
|
- MesloLGM Nerd Font (& Mono)
|
||||||
- WenQuanYi Micro Hei
|
- WenQuanYi Micro Hei
|
||||||
- Sour Gummy
|
- Sour Gummy
|
||||||
- Noto Sans
|
- Noto Sans
|
||||||
|
|||||||
Submodule backgrounds updated: 49aad19814...ec1022ffec
@@ -27,7 +27,7 @@ input {
|
|||||||
warp-mouse-to-focus
|
warp-mouse-to-focus
|
||||||
|
|
||||||
// Focus windows and outputs automatically when moving the mouse into them.
|
// Focus windows and outputs automatically when moving the mouse into them.
|
||||||
focus-follows-mouse max-scroll-amount="75%"
|
focus-follows-mouse max-scroll-amount="100%"
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************Output************************/
|
/************************Output************************/
|
||||||
@@ -114,11 +114,6 @@ layer-rule {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
layer-rule {
|
|
||||||
match namespace="^quickshell-bar$"
|
|
||||||
place-within-backdrop false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/************************Autostart************************/
|
/************************Autostart************************/
|
||||||
|
|
||||||
@@ -126,7 +121,7 @@ layer-rule {
|
|||||||
spawn-sh-at-startup "config-switch niri"
|
spawn-sh-at-startup "config-switch niri"
|
||||||
|
|
||||||
// Bar
|
// Bar
|
||||||
spawn-at-startup "waybar"
|
spawn-at-startup "quickshell"
|
||||||
|
|
||||||
// Wallpaper
|
// Wallpaper
|
||||||
spawn-at-startup "wallpaper-daemon"
|
spawn-at-startup "wallpaper-daemon"
|
||||||
@@ -141,7 +136,7 @@ spawn-sh-at-startup "gnome-keyring-daemon --start --components=secrets"
|
|||||||
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
|
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
|
||||||
spawn-at-startup "mako"
|
spawn-at-startup "mako"
|
||||||
|
|
||||||
// idle
|
// Idle
|
||||||
spawn-sh-at-startup "hypridle"
|
spawn-sh-at-startup "hypridle"
|
||||||
|
|
||||||
// Clipboard history
|
// Clipboard history
|
||||||
@@ -286,8 +281,8 @@ binds {
|
|||||||
Mod+Shift+W { spawn "wallpaper-chooser"; }
|
Mod+Shift+W { spawn "wallpaper-chooser"; }
|
||||||
|
|
||||||
// EWW
|
// EWW
|
||||||
Mod+Space { spawn-sh "eww open main --toggle"; }
|
Mod+Space { spawn-sh "qs ipc call panels toggleControlCenter"; }
|
||||||
Mod+Shift+L { spawn-sh "lyrics-widgets"; }
|
Mod+Shift+L { spawn-sh "qs ipc call lyrics toggleBarLyrics"; }
|
||||||
|
|
||||||
// Waybar
|
// Waybar
|
||||||
Mod+Shift+K { spawn-sh "waybar-toggle"; }
|
Mod+Shift+K { spawn-sh "waybar-toggle"; }
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ input {
|
|||||||
warp-mouse-to-focus
|
warp-mouse-to-focus
|
||||||
|
|
||||||
// Focus windows and outputs automatically when moving the mouse into them.
|
// Focus windows and outputs automatically when moving the mouse into them.
|
||||||
focus-follows-mouse max-scroll-amount="75%"
|
focus-follows-mouse max-scroll-amount="100%"
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************Output************************/
|
/************************Output************************/
|
||||||
@@ -121,7 +121,7 @@ layer-rule {
|
|||||||
spawn-sh-at-startup "config-switch niri"
|
spawn-sh-at-startup "config-switch niri"
|
||||||
|
|
||||||
// Bar
|
// Bar
|
||||||
spawn-at-startup "waybar"
|
spawn-at-startup "quickshell"
|
||||||
|
|
||||||
// Wallpaper
|
// Wallpaper
|
||||||
spawn-at-startup "wallpaper-daemon"
|
spawn-at-startup "wallpaper-daemon"
|
||||||
@@ -136,7 +136,7 @@ spawn-sh-at-startup "gnome-keyring-daemon --start --components=secrets"
|
|||||||
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
|
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
|
||||||
spawn-at-startup "mako"
|
spawn-at-startup "mako"
|
||||||
|
|
||||||
// idle
|
// Idle
|
||||||
spawn-sh-at-startup "hypridle"
|
spawn-sh-at-startup "hypridle"
|
||||||
|
|
||||||
// Clipboard history
|
// Clipboard history
|
||||||
@@ -281,8 +281,8 @@ binds {
|
|||||||
Mod+Shift+W { spawn "wallpaper-chooser"; }
|
Mod+Shift+W { spawn "wallpaper-chooser"; }
|
||||||
|
|
||||||
// EWW
|
// EWW
|
||||||
Mod+Space { spawn-sh "eww open main --toggle"; }
|
Mod+Space { spawn-sh "qs ipc call panels toggleControlCenter"; }
|
||||||
Mod+Shift+L { spawn-sh "lyrics-widgets"; }
|
Mod+Shift+L { spawn-sh "qs ipc call lyrics toggleBarLyrics"; }
|
||||||
|
|
||||||
// Waybar
|
// Waybar
|
||||||
Mod+Shift+K { spawn-sh "waybar-toggle"; }
|
Mod+Shift+K { spawn-sh "waybar-toggle"; }
|
||||||
|
|||||||
4
quickshell/Assets/Config/.gitignore
vendored
Normal file
4
quickshell/Assets/Config/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
Location.json
|
||||||
|
|
||||||
|
GeoInfoToken.txt
|
||||||
1
quickshell/Assets/Config/LyricsOffset.txt
Normal file
1
quickshell/Assets/Config/LyricsOffset.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
4
quickshell/Assets/Config/Settings.json
Normal file
4
quickshell/Assets/Config/Settings.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"primaryColor": "#89b4fa",
|
||||||
|
"showLyricsBar": true
|
||||||
|
}
|
||||||
BIN
quickshell/Assets/Images/Avatar.jpg
Normal file
BIN
quickshell/Assets/Images/Avatar.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
2
quickshell/Assets/Ip/.gitignore
vendored
2
quickshell/Assets/Ip/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
token.txt
|
|
||||||
# cache.json
|
|
||||||
25
quickshell/Constants/Color.qml
Normal file
25
quickshell/Constants/Color.qml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Constants
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property color mPrimary: Colors.primary
|
||||||
|
property color mOnPrimary: Colors.base
|
||||||
|
property color mSecondary: Colors.primary
|
||||||
|
property color mOnSecondary: Colors.base
|
||||||
|
property color mTertiary: Colors.primary
|
||||||
|
property color mOnTertiary: Colors.base
|
||||||
|
property color mError: Colors.red
|
||||||
|
property color mOnError: Colors.base
|
||||||
|
property color mSurface: Colors.base
|
||||||
|
property color mOnSurface: Colors.text
|
||||||
|
property color mSurfaceVariant: Colors.surface
|
||||||
|
property color mOnSurfaceVariant: Colors.text
|
||||||
|
property color mOutline: Colors.primary
|
||||||
|
property color mShadow: Colors.crust
|
||||||
|
property color transparent: "transparent"
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import qs.Services
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -29,10 +30,11 @@ Singleton {
|
|||||||
readonly property color surface2: "#585b70"
|
readonly property color surface2: "#585b70"
|
||||||
readonly property color surface1: "#45475a"
|
readonly property color surface1: "#45475a"
|
||||||
readonly property color surface0: "#313244"
|
readonly property color surface0: "#313244"
|
||||||
|
readonly property color surface: "#292a3c"
|
||||||
readonly property color base: "#1e1e2e"
|
readonly property color base: "#1e1e2e"
|
||||||
readonly property color mantle: "#181825"
|
readonly property color mantle: "#181825"
|
||||||
readonly property color crust: "#11111b"
|
readonly property color crust: "#11111b"
|
||||||
readonly property color accent: "#89b4fa"
|
property color primary: SettingsService.primaryColor
|
||||||
readonly property color distroColor: "#74c7ec"
|
readonly property color distroColor: "#74c7ec"
|
||||||
readonly property var cavaList: ["#b4befe", "#89b4fa", "#74c7ec", "#89dceb", "#94e2d5", "#a6e3a1", "#f9e2af", "#fab387"]
|
readonly property var cavaList: ["#b4befe", "#89b4fa", "#74c7ec", "#89dceb", "#94e2d5", "#a6e3a1", "#f9e2af", "#fab387"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import qs.Constants
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -8,8 +9,8 @@ Singleton {
|
|||||||
readonly property string primary: "Sour Gummy Light"
|
readonly property string primary: "Sour Gummy Light"
|
||||||
readonly property string nerd: "Meslo LGM Nerd Font Mono"
|
readonly property string nerd: "Meslo LGM Nerd Font Mono"
|
||||||
readonly property string sans: "Noto Sans"
|
readonly property string sans: "Noto Sans"
|
||||||
readonly property int small: 10
|
readonly property int small: Style.fontSizeS
|
||||||
readonly property int medium: 12
|
readonly property int medium: Style.fontSizeM
|
||||||
readonly property int large: 14
|
readonly property int large: Style.fontSizeL
|
||||||
readonly property int icon: 14
|
readonly property int icon: 14 // for nerd font
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import qs.Utils
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -30,6 +31,11 @@ Singleton {
|
|||||||
readonly property string global: ""
|
readonly property string global: ""
|
||||||
readonly property string upload: ""
|
readonly property string upload: ""
|
||||||
readonly property string download: ""
|
readonly property string download: ""
|
||||||
|
readonly property string speedSlower: ""
|
||||||
|
readonly property string speedFaster: ""
|
||||||
|
readonly property string speedReset: ""
|
||||||
|
readonly property string reset: ""
|
||||||
|
readonly property string lines: ""
|
||||||
// Expose the font family name for easy access
|
// Expose the font family name for easy access
|
||||||
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
|
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
|
||||||
readonly property string defaultIcon: TablerIcons.defaultIcon
|
readonly property string defaultIcon: TablerIcons.defaultIcon
|
||||||
@@ -73,7 +79,7 @@ Singleton {
|
|||||||
if (currentFontLoader.status === FontLoader.Ready)
|
if (currentFontLoader.status === FontLoader.Ready)
|
||||||
fontReloaded();
|
fontReloaded();
|
||||||
else if (currentFontLoader.status === FontLoader.Error)
|
else if (currentFontLoader.status === FontLoader.Error)
|
||||||
Logger.error("Font failed to load (version " + fontVersion + ")");
|
Logger.error("Icons", "Font failed to load (version " + fontVersion + ")");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
68
quickshell/Constants/Style.qml
Normal file
68
quickshell/Constants/Style.qml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
/*
|
||||||
|
Preset sizes for font, radii, ?
|
||||||
|
*/
|
||||||
|
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Font size
|
||||||
|
property real fontSizeXXS: 8
|
||||||
|
property real fontSizeXS: 9
|
||||||
|
property real fontSizeS: 10
|
||||||
|
property real fontSizeM: 11
|
||||||
|
property real fontSizeL: 13
|
||||||
|
property real fontSizeXL: 16
|
||||||
|
property real fontSizeXXL: 18
|
||||||
|
property real fontSizeXXXL: 24
|
||||||
|
// Font weight
|
||||||
|
property int fontWeightRegular: 400
|
||||||
|
property int fontWeightMedium: 500
|
||||||
|
property int fontWeightSemiBold: 600
|
||||||
|
property int fontWeightBold: 700
|
||||||
|
// Radii
|
||||||
|
property int radiusXXS: 4
|
||||||
|
property int radiusXS: 8
|
||||||
|
property int radiusS: 12
|
||||||
|
property int radiusM: 16
|
||||||
|
property int radiusL: 20
|
||||||
|
//screen Radii
|
||||||
|
property int screenRadius: 20
|
||||||
|
// Border
|
||||||
|
property int borderS: 2
|
||||||
|
property int borderM: 3
|
||||||
|
property int borderL: 4
|
||||||
|
// Margins (for margins and spacing)
|
||||||
|
property int marginXXS: 2
|
||||||
|
property int marginXS: 4
|
||||||
|
property int marginS: 8
|
||||||
|
property int marginM: 12
|
||||||
|
property int marginL: 16
|
||||||
|
property int marginXL: 24
|
||||||
|
// Opacity
|
||||||
|
property real opacityNone: 0
|
||||||
|
property real opacityLight: 0.25
|
||||||
|
property real opacityMedium: 0.5
|
||||||
|
property real opacityHeavy: 0.75
|
||||||
|
property real opacityAlmost: 0.95
|
||||||
|
property real opacityFull: 1
|
||||||
|
// Animation duration (ms)
|
||||||
|
property int animationFast: 150
|
||||||
|
property int animationNormal: 300
|
||||||
|
property int animationSlow: 450
|
||||||
|
property int animationSlowest: 750
|
||||||
|
// Delays
|
||||||
|
property int tooltipDelay: 300
|
||||||
|
property int tooltipDelayLong: 1200
|
||||||
|
property int pillDelay: 500
|
||||||
|
// Settings widgets base size
|
||||||
|
property real baseWidgetSize: 33
|
||||||
|
property real sliderWidth: 200
|
||||||
|
// Bar Dimensions
|
||||||
|
property real barHeight: 45
|
||||||
|
property real capsuleHeight: 35
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ Scope {
|
|||||||
screen: modelData
|
screen: modelData
|
||||||
WlrLayershell.namespace: "quickshell-bar"
|
WlrLayershell.namespace: "quickshell-bar"
|
||||||
color: Colors.transparent
|
color: Colors.transparent
|
||||||
implicitHeight: 45
|
implicitHeight: Style.barHeight
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
left: true
|
left: true
|
||||||
@@ -94,6 +94,9 @@ Scope {
|
|||||||
SymbolButton {
|
SymbolButton {
|
||||||
symbol: Icons.distro
|
symbol: Icons.distro
|
||||||
buttonColor: Colors.distroColor
|
buttonColor: Colors.distroColor
|
||||||
|
onClicked: {
|
||||||
|
PanelService.getPanel("controlCenterPanel")?.toggle(this)
|
||||||
|
}
|
||||||
onRightClicked: {
|
onRightClicked: {
|
||||||
if (action.running) {
|
if (action.running) {
|
||||||
action.signal(15);
|
action.signal(15);
|
||||||
@@ -164,37 +167,49 @@ Scope {
|
|||||||
rightMargin: 5
|
rightMargin: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkSpeed {
|
RowLayout {
|
||||||
|
id: monitorsLayout
|
||||||
|
visible: !SettingsService.showLyricsBar
|
||||||
|
|
||||||
|
height: parent.height
|
||||||
|
NetworkSpeed {
|
||||||
|
}
|
||||||
|
|
||||||
|
Separator {
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
Ip {
|
||||||
|
showCountryCode: true
|
||||||
|
}
|
||||||
|
|
||||||
|
CpuTemp {
|
||||||
|
}
|
||||||
|
|
||||||
|
MemUsage {
|
||||||
|
}
|
||||||
|
|
||||||
|
CpuUsage {
|
||||||
|
}
|
||||||
|
|
||||||
|
Battery {
|
||||||
|
}
|
||||||
|
|
||||||
|
Brightness {
|
||||||
|
screen: modelData
|
||||||
|
}
|
||||||
|
|
||||||
|
Volume {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Separator {
|
LyricsBar {
|
||||||
}
|
id: lyricsBar
|
||||||
|
visible: SettingsService.showLyricsBar
|
||||||
Item {
|
width: 600
|
||||||
width: 10
|
|
||||||
}
|
|
||||||
|
|
||||||
Ip {
|
|
||||||
showCountryCode: true
|
|
||||||
}
|
|
||||||
|
|
||||||
CpuTemp {
|
|
||||||
}
|
|
||||||
|
|
||||||
MemUsage {
|
|
||||||
}
|
|
||||||
|
|
||||||
CpuUsage {
|
|
||||||
}
|
|
||||||
|
|
||||||
Battery {
|
|
||||||
}
|
|
||||||
|
|
||||||
Brightness {
|
|
||||||
screen: modelData
|
|
||||||
}
|
|
||||||
|
|
||||||
Volume {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import QtQuick
|
|||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import qs.Constants
|
import qs.Constants
|
||||||
import qs.Modules.Misc
|
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
font.pointSize: Fonts.medium
|
font.pointSize: Fonts.medium
|
||||||
font.family: Fonts.primary
|
font.family: Fonts.primary
|
||||||
color: Colors.accent
|
color: Colors.primary
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: action
|
id: action
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: Icons.global
|
text: Icons.global
|
||||||
font.pointSize: Fonts.icon + 5
|
font.pointSize: Fonts.icon + 6
|
||||||
color: Colors.peach
|
color: Colors.peach
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ Item {
|
|||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: 200
|
duration: Style.animationFast
|
||||||
easing.type: Easing.InOutCubic
|
easing.type: Easing.InOutCubic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
88
quickshell/Modules/Bar/Components/LyricsBar.qml
Normal file
88
quickshell/Modules/Bar/Components/LyricsBar.qml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: parent.height
|
||||||
|
radius: Style.radiusS
|
||||||
|
color: Colors.base
|
||||||
|
border.color: Colors.primary
|
||||||
|
border.width: Style.borderS
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SettingsService
|
||||||
|
onShowLyricsBarChanged: {
|
||||||
|
visible = SettingsService.showLyricsBar;
|
||||||
|
if (visible)
|
||||||
|
LyricsService.startSyncing();
|
||||||
|
else
|
||||||
|
LyricsService.stopSyncing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Style.marginM
|
||||||
|
anchors.rightMargin: Style.marginM
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
implicitWidth: parent.width - slowerButton.implicitWidth * 3 - parent.spacing * 3 - parent.anchors.leftMargin - parent.anchors.rightMargin
|
||||||
|
Layout.fillHeight: true
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: LyricsService.lyrics[LyricsService.currentIndex] || ""
|
||||||
|
family: Fonts.sans
|
||||||
|
pointSize: Style.fontSizeS
|
||||||
|
maximumLineCount: 1
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: slowerButton
|
||||||
|
|
||||||
|
baseSize: 24
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorBgHover: Colors.blue
|
||||||
|
colorFg: Colors.blue
|
||||||
|
icon: "rotate-2"
|
||||||
|
onClicked: {
|
||||||
|
LyricsService.increaseOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: playPauseButton
|
||||||
|
|
||||||
|
baseSize: 24
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorBgHover: Colors.yellow
|
||||||
|
colorFg: Colors.yellow
|
||||||
|
icon: "rotate-clockwise-2"
|
||||||
|
onClicked: {
|
||||||
|
LyricsService.decreaseOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: nextButton
|
||||||
|
|
||||||
|
baseSize: 24
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorBgHover: Colors.green
|
||||||
|
colorFg: Colors.green
|
||||||
|
icon: "rotate-clockwise"
|
||||||
|
onClicked: {
|
||||||
|
LyricsService.resetOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,15 +19,15 @@ Item {
|
|||||||
Text {
|
Text {
|
||||||
text: Icons.download
|
text: Icons.download
|
||||||
font.pointSize: Fonts.icon - 3
|
font.pointSize: Fonts.icon - 3
|
||||||
color: Colors.accent
|
color: Colors.primary
|
||||||
Layout.leftMargin: 10
|
Layout.leftMargin: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||||
font.pointSize: Fonts.medium
|
font.pointSize: Fonts.medium
|
||||||
font.family: Fonts.primary
|
font.family: Fonts.primary
|
||||||
color: Colors.accent
|
color: Colors.primary
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -37,14 +37,14 @@ Item {
|
|||||||
Text {
|
Text {
|
||||||
text: Icons.upload
|
text: Icons.upload
|
||||||
font.pointSize: Fonts.icon - 3
|
font.pointSize: Fonts.icon - 3
|
||||||
color: Colors.accent
|
color: Colors.primary
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||||
font.pointSize: Fonts.medium
|
font.pointSize: Fonts.medium
|
||||||
font.family: Fonts.primary
|
font.family: Fonts.primary
|
||||||
color: Colors.accent
|
color: Colors.primary
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,13 @@ Text {
|
|||||||
text: TimeService.time + " | " + TimeService.dateString
|
text: TimeService.time + " | " + TimeService.dateString
|
||||||
font.pointSize: Fonts.medium
|
font.pointSize: Fonts.medium
|
||||||
font.family: Fonts.primary
|
font.family: Fonts.primary
|
||||||
color: Colors.accent
|
color: Colors.primary
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
PanelService.getPanel("calendarPanel")?.toggle(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ Item {
|
|||||||
property ListModel localWorkspaces
|
property ListModel localWorkspaces
|
||||||
property real masterProgress: 0
|
property real masterProgress: 0
|
||||||
property bool effectsActive: false
|
property bool effectsActive: false
|
||||||
property color effectColor: Colors.accent
|
property color effectColor: Colors.primary
|
||||||
property int horizontalPadding: 16
|
property int horizontalPadding: 16
|
||||||
property int spacingBetweenPills: 8
|
property int spacingBetweenPills: 8
|
||||||
property bool isDestroying: false
|
property bool isDestroying: false
|
||||||
|
|
||||||
signal workspaceChanged(int workspaceId, color accentColor)
|
signal workspaceChanged(int workspaceId, color primaryColor)
|
||||||
|
|
||||||
function triggerUnifiedWave() {
|
function triggerUnifiedWave() {
|
||||||
effectColor = Colors.accent;
|
effectColor = Colors.primary;
|
||||||
masterAnimation.restart();
|
masterAnimation.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ Item {
|
|||||||
const ws = localWorkspaces.get(i);
|
const ws = localWorkspaces.get(i);
|
||||||
if (ws.isFocused === true) {
|
if (ws.isFocused === true) {
|
||||||
root.triggerUnifiedWave();
|
root.triggerUnifiedWave();
|
||||||
root.workspaceChanged(ws.id, Colors.accent);
|
root.workspaceChanged(ws.id, Colors.primary);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,10 +180,10 @@ Item {
|
|||||||
}
|
}
|
||||||
color: {
|
color: {
|
||||||
if (model.isFocused)
|
if (model.isFocused)
|
||||||
return Colors.accent;
|
return Colors.primary;
|
||||||
|
|
||||||
if (model.isActive)
|
if (model.isActive)
|
||||||
return Colors.accent.lighter(130);
|
return Colors.primary.lighter(130);
|
||||||
|
|
||||||
if (model.isUrgent)
|
if (model.isUrgent)
|
||||||
return Theme.error;
|
return Theme.error;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Item {
|
|||||||
property real maxValue: 100
|
property real maxValue: 100
|
||||||
property real value: 100
|
property real value: 100
|
||||||
property string textValue: "" // override value in textDisplay if set
|
property string textValue: "" // override value in textDisplay if set
|
||||||
property color fillColor: Colors.accent
|
property color fillColor: Colors.primary
|
||||||
property string textSuffix: ""
|
property string textSuffix: ""
|
||||||
property bool pointerCursor: true
|
property bool pointerCursor: true
|
||||||
property alias hovered: mouseArea.containsMouse
|
property alias hovered: mouseArea.containsMouse
|
||||||
@@ -127,7 +127,7 @@ Item {
|
|||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: 200
|
duration: Style.animationNormal
|
||||||
easing.type: Easing.InOutCubic
|
easing.type: Easing.InOutCubic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ Item {
|
|||||||
required property string symbol
|
required property string symbol
|
||||||
property color buttonColor: Colors.distroColor
|
property color buttonColor: Colors.distroColor
|
||||||
readonly property alias hovered: mouseArea.containsMouse
|
readonly property alias hovered: mouseArea.containsMouse
|
||||||
|
property real iconSize: Fonts.icon
|
||||||
|
property real radius: Style.radiusS
|
||||||
|
|
||||||
signal clicked()
|
signal clicked()
|
||||||
signal rightClicked()
|
signal rightClicked()
|
||||||
@@ -35,7 +37,7 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
text: symbol
|
text: symbol
|
||||||
font.family: Fonts.nerd
|
font.family: Fonts.nerd
|
||||||
font.pointSize: Fonts.icon
|
font.pointSize: iconSize
|
||||||
font.bold: false
|
font.bold: false
|
||||||
color: buttonColor
|
color: buttonColor
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
@@ -46,11 +48,12 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: parent.hovered ? buttonColor : Colors.transparent
|
color: parent.hovered ? buttonColor : Colors.transparent
|
||||||
opacity: 0.3
|
opacity: 0.3
|
||||||
radius: 14
|
radius: root.radius
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
ColorAnimation {
|
ColorAnimation {
|
||||||
duration: 120
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InOutCubic
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Quickshell.Widgets
|
|||||||
import qs.Modules.Bar.Misc
|
import qs.Modules.Bar.Misc
|
||||||
import qs.Constants
|
import qs.Constants
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
@@ -107,8 +108,7 @@ Rectangle {
|
|||||||
trayMenu.item.menu = modelData.menu
|
trayMenu.item.menu = modelData.menu
|
||||||
trayMenu.item.showAt(parent, menuX, menuY)
|
trayMenu.item.showAt(parent, menuX, menuY)
|
||||||
} else {
|
} else {
|
||||||
// Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
|
Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
|
||||||
console.log("No menu available for", modelData.id, "or trayMenu not set")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import QtQuick.Controls
|
|||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Constants
|
import qs.Constants
|
||||||
|
import qs.Utils
|
||||||
|
import qs.Noctalia
|
||||||
|
|
||||||
PopupWindow {
|
PopupWindow {
|
||||||
id: root
|
id: root
|
||||||
@@ -19,7 +21,7 @@ PopupWindow {
|
|||||||
implicitWidth: menuWidth
|
implicitWidth: menuWidth
|
||||||
|
|
||||||
// Use the content height of the Flickable for implicit height
|
// Use the content height of the Flickable for implicit height
|
||||||
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + 20)
|
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + (Style.marginS * 2))
|
||||||
visible: false
|
visible: false
|
||||||
color: Colors.transparent
|
color: Colors.transparent
|
||||||
anchor.item: anchorItem
|
anchor.item: anchorItem
|
||||||
@@ -28,7 +30,7 @@ PopupWindow {
|
|||||||
|
|
||||||
function showAt(item, x, y) {
|
function showAt(item, x, y) {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
console.warn("AnchorItem is undefined, won't show menu.")
|
Logger.warn("TrayMenu", "AnchorItem is undefined, won't show menu.");
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,15 +86,15 @@ PopupWindow {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Colors.base
|
color: Colors.base
|
||||||
border.color: Colors.accent
|
border.color: Colors.primary
|
||||||
border.width: 2
|
border.width: 2
|
||||||
radius: 14
|
radius: Style.radiusM
|
||||||
}
|
}
|
||||||
|
|
||||||
Flickable {
|
Flickable {
|
||||||
id: flickable
|
id: flickable
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 10
|
anchors.margins: Style.marginS
|
||||||
contentHeight: columnLayout.implicitHeight
|
contentHeight: columnLayout.implicitHeight
|
||||||
interactive: true
|
interactive: true
|
||||||
|
|
||||||
@@ -115,88 +117,56 @@ PopupWindow {
|
|||||||
return 8
|
return 8
|
||||||
} else {
|
} else {
|
||||||
// Calculate based on text content
|
// Calculate based on text content
|
||||||
const textHeight = text.contentHeight || (Fonts.small)
|
const textHeight = text.contentHeight || (Style.fontSizeS * 1.2)
|
||||||
return textHeight + 16
|
return Math.max(28, textHeight + (Style.marginS * 2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
color: Colors.transparent
|
color: Colors.transparent
|
||||||
property var subMenu: null
|
property var subMenu: null
|
||||||
|
|
||||||
Rectangle {
|
NDivider {
|
||||||
width: parent.width - 16
|
|
||||||
height: 1
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - (Style.marginM * 2)
|
||||||
visible: modelData?.isSeparator ?? false
|
visible: modelData?.isSeparator ?? false
|
||||||
gradient: Gradient {
|
|
||||||
orientation: Gradient.Horizontal
|
|
||||||
GradientStop {
|
|
||||||
position: 0.0
|
|
||||||
color: Colors.transparent
|
|
||||||
}
|
|
||||||
GradientStop {
|
|
||||||
position: 0.1
|
|
||||||
color: Colors.accent
|
|
||||||
}
|
|
||||||
GradientStop {
|
|
||||||
position: 0.9
|
|
||||||
color: Colors.accent
|
|
||||||
}
|
|
||||||
GradientStop {
|
|
||||||
position: 1.0
|
|
||||||
color: Colors.transparent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: mouseArea.containsMouse ? Colors.accent : Colors.transparent
|
color: mouseArea.containsMouse ? Colors.primary : Colors.transparent
|
||||||
radius: 10
|
radius: Style.radiusS
|
||||||
visible: !(modelData?.isSeparator ?? false)
|
visible: !(modelData?.isSeparator ?? false)
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: 8
|
anchors.leftMargin: Style.marginM
|
||||||
anchors.rightMargin: 8
|
anchors.rightMargin: Style.marginM
|
||||||
spacing: 8
|
spacing: Style.marginS
|
||||||
|
|
||||||
Text {
|
NText {
|
||||||
id: text
|
id: text
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Colors.base : Colors.text) : Colors.text
|
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
|
||||||
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
|
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
|
||||||
font.pointSize: Fonts.small
|
pointSize: Style.fontSizeS
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
Layout.preferredWidth: 14
|
Layout.preferredWidth: Style.marginL
|
||||||
Layout.preferredHeight: 14
|
Layout.preferredHeight: Style.marginL
|
||||||
source: modelData?.icon ?? ""
|
source: modelData?.icon ?? ""
|
||||||
visible: (modelData?.icon ?? "") !== ""
|
visible: (modelData?.icon ?? "") !== ""
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
NIcon {
|
||||||
|
icon: modelData?.hasChildren ? "menu" : ""
|
||||||
|
pointSize: Style.fontSizeS
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
visible: modelData?.hasChildren ?? false
|
visible: modelData?.hasChildren ?? false
|
||||||
color: (mouseArea.containsMouse ? Colors.base : Colors.text)
|
color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
|
||||||
|
|
||||||
text: {
|
|
||||||
const icon = modelData?.hasChildren ? "menu" : ""
|
|
||||||
if ((icon === undefined) || (icon === "")) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if (Icons.get(icon) === undefined) {
|
|
||||||
console.warn("Icon", `"${icon}"`, "doesn't exist in the icons font")
|
|
||||||
return Icons.get(Icons.defaultIcon)
|
|
||||||
}
|
|
||||||
return Icons.get(icon)
|
|
||||||
}
|
|
||||||
font.family: Icons.fontFamily
|
|
||||||
font.pointSize: Fonts.small
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
537
quickshell/Modules/Panel/CalendarPanel.qml
Normal file
537
quickshell/Modules/Panel/CalendarPanel.qml
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
NPanel {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
preferredWidth: 400
|
||||||
|
preferredHeight: 520
|
||||||
|
|
||||||
|
panelContent: ColumnLayout {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
readonly property int firstDayOfWeek: Qt.locale().firstDayOfWeek
|
||||||
|
property bool isCurrentMonth: checkIsCurrentMonth()
|
||||||
|
readonly property bool weatherReady: (LocationService.data.weather !== null)
|
||||||
|
|
||||||
|
function checkIsCurrentMonth() {
|
||||||
|
return (Time.date.getMonth() === grid.month) && (Time.date.getFullYear() === grid.year);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getISOWeekNumber(date) {
|
||||||
|
const target = new Date(date.getTime());
|
||||||
|
target.setHours(0, 0, 0, 0);
|
||||||
|
const dayOfWeek = target.getDay() || 7;
|
||||||
|
target.setDate(target.getDate() + 4 - dayOfWeek);
|
||||||
|
const yearStart = new Date(target.getFullYear(), 0, 1);
|
||||||
|
const weekNumber = Math.ceil(((target - yearStart) / 8.64e+07 + 1) / 7);
|
||||||
|
return weekNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginL
|
||||||
|
spacing: Style.marginM
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onDateChanged() {
|
||||||
|
isCurrentMonth = checkIsCurrentMonth();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combined blue banner with date/time and weather summary
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: blueColumn.implicitHeight + Style.marginM * 2
|
||||||
|
radius: Style.radiusL
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
layer.enabled: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: blueColumn
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginM
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// Combined layout for weather icon, date, and weather text
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 60
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
// Weather icon and temperature
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
spacing: Style.marginXXS
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "cloud"
|
||||||
|
pointSize: Style.fontSizeXXL
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: {
|
||||||
|
if (!weatherReady)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
var temp = LocationService.data.weather.current_weather.temperature;
|
||||||
|
var suffix = "C";
|
||||||
|
temp = Math.round(temp);
|
||||||
|
return `${temp}°${suffix}`;
|
||||||
|
}
|
||||||
|
pointSize: Style.fontSizeM
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Today day number
|
||||||
|
NText {
|
||||||
|
visible: content.isCurrentMonth
|
||||||
|
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||||
|
text: Time.date.getDate()
|
||||||
|
pointSize: Style.fontSizeXXXL * 1.5
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
visible: !content.isCurrentMonth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Month, year, location
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: false
|
||||||
|
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||||
|
spacing: -Style.marginXS
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: Qt.locale().monthName(grid.month, Locale.LongFormat).toUpperCase()
|
||||||
|
pointSize: Style.fontSizeXL * 1.2
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignBaseline
|
||||||
|
Layout.maximumWidth: 150
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: ` ${grid.year}`
|
||||||
|
pointSize: Style.fontSizeL
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Qt.alpha(Color.mOnSurfaceVariant, 0.7)
|
||||||
|
Layout.alignment: Qt.AlignBaseline
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: {
|
||||||
|
if (!weatherReady)
|
||||||
|
return "Weather unavailable";
|
||||||
|
|
||||||
|
const chunks = LocationService.data.name.split(",");
|
||||||
|
return chunks[0];
|
||||||
|
}
|
||||||
|
pointSize: Style.fontSizeM
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.maximumWidth: 150
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: weatherReady ? ` (${LocationService.data.weather.timezone_abbreviation})` : ""
|
||||||
|
pointSize: Style.fontSizeXS
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
color: Qt.alpha(Color.mOnSurfaceVariant, 0.7)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer between date and clock
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Digital clock with circular progress
|
||||||
|
Item {
|
||||||
|
width: Style.fontSizeXXXL * 1.9
|
||||||
|
height: Style.fontSizeXXXL * 1.9
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
|
// Seconds circular progress
|
||||||
|
Canvas {
|
||||||
|
id: secondsProgress
|
||||||
|
|
||||||
|
property real progress: Time.date.getSeconds() / 60
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
onProgressChanged: requestPaint()
|
||||||
|
onPaint: {
|
||||||
|
var ctx = getContext("2d");
|
||||||
|
var centerX = width / 2;
|
||||||
|
var centerY = height / 2;
|
||||||
|
var radius = Math.min(width, height) / 2 - 3;
|
||||||
|
ctx.reset();
|
||||||
|
// Background circle
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
||||||
|
ctx.lineWidth = 2.5;
|
||||||
|
ctx.strokeStyle = Qt.alpha(Color.mOnSurfaceVariant, 0.15);
|
||||||
|
ctx.stroke();
|
||||||
|
// Progress arc
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI);
|
||||||
|
ctx.lineWidth = 2.5;
|
||||||
|
ctx.strokeStyle = Color.mOnSurfaceVariant;
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onDateChanged() {
|
||||||
|
secondsProgress.progress = Time.date.getSeconds() / 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Time
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Digital clock
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: -Style.marginXXS
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: {
|
||||||
|
var t = Qt.locale().toString(new Date(), "HH");
|
||||||
|
return t.split(" ")[0];
|
||||||
|
}
|
||||||
|
pointSize: Style.fontSizeXS
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
family: Fonts.sans
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: Qt.formatTime(Time.date, "mm")
|
||||||
|
pointSize: Style.fontSizeXXS
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
family: Fonts.sans
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.effect: DropShadow {
|
||||||
|
horizontalOffset: 6
|
||||||
|
verticalOffset: 6
|
||||||
|
radius: 8
|
||||||
|
samples: 12
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6-day forecast (outside blue banner)
|
||||||
|
RowLayout {
|
||||||
|
visible: weatherReady
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
spacing: Style.marginL
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: weatherReady ? Math.min(6, LocationService.data.weather.daily.time.length) : 0
|
||||||
|
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
Layout.preferredWidth: 0
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: {
|
||||||
|
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"));
|
||||||
|
return Qt.locale().toString(weatherDate, "ddd");
|
||||||
|
}
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
pointSize: Style.fontSizeM
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
|
||||||
|
icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index])
|
||||||
|
pointSize: Style.fontSizeXXL * 1.5
|
||||||
|
color: LocationService.weatherColorFromCode(LocationService.data.weather.daily.weathercode[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: {
|
||||||
|
var max = LocationService.data.weather.daily.temperature_2m_max[index];
|
||||||
|
var min = LocationService.data.weather.daily.temperature_2m_min[index];
|
||||||
|
max = Math.round(max);
|
||||||
|
min = Math.round(min);
|
||||||
|
return `${max}°/${min}°`;
|
||||||
|
}
|
||||||
|
pointSize: Style.fontSizeXS
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading indicator for weather
|
||||||
|
RowLayout {
|
||||||
|
visible: !weatherReady
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
|
NBusyIndicator {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer
|
||||||
|
Item {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation and divider
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
NDivider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "chevron-left"
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorBorder: Color.transparent
|
||||||
|
colorBorderHover: Color.transparent
|
||||||
|
onClicked: {
|
||||||
|
let newDate = new Date(grid.year, grid.month - 1, 1);
|
||||||
|
grid.year = newDate.getFullYear();
|
||||||
|
grid.month = newDate.getMonth();
|
||||||
|
content.isCurrentMonth = content.checkIsCurrentMonth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "calendar"
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorBorder: Color.transparent
|
||||||
|
colorBorderHover: Color.transparent
|
||||||
|
onClicked: {
|
||||||
|
grid.month = Time.date.getMonth();
|
||||||
|
grid.year = Time.date.getFullYear();
|
||||||
|
content.isCurrentMonth = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "chevron-right"
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorBorder: Color.transparent
|
||||||
|
colorBorderHover: Color.transparent
|
||||||
|
onClicked: {
|
||||||
|
let newDate = new Date(grid.year, grid.month + 1, 1);
|
||||||
|
grid.year = newDate.getFullYear();
|
||||||
|
grid.month = newDate.getMonth();
|
||||||
|
content.isCurrentMonth = content.checkIsCurrentMonth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names of days of the week
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
columns: 7
|
||||||
|
rows: 1
|
||||||
|
columnSpacing: 0
|
||||||
|
rowSpacing: 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: 7
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Style.baseWidgetSize * 0.6
|
||||||
|
|
||||||
|
NText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: {
|
||||||
|
let dayIndex = (content.firstDayOfWeek + index) % 7;
|
||||||
|
const dayNames = ["S", "M", "T", "W", "T", "F", "S"];
|
||||||
|
return dayNames[dayIndex];
|
||||||
|
}
|
||||||
|
color: Color.mPrimary
|
||||||
|
pointSize: Style.fontSizeS
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid with weeks and days
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// Column of week numbers
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
|
||||||
|
Layout.fillHeight: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: 6
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
NText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
color: Color.mOutline
|
||||||
|
pointSize: Style.fontSizeXXS
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
text: {
|
||||||
|
let firstOfMonth = new Date(grid.year, grid.month, 1);
|
||||||
|
let firstDayOfWeek = content.firstDayOfWeek;
|
||||||
|
let firstOfMonthDayOfWeek = firstOfMonth.getDay();
|
||||||
|
let daysBeforeFirst = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7;
|
||||||
|
if (daysBeforeFirst === 0)
|
||||||
|
daysBeforeFirst = 7;
|
||||||
|
|
||||||
|
let gridStartDate = new Date(grid.year, grid.month, 1 - daysBeforeFirst);
|
||||||
|
let rowStartDate = new Date(gridStartDate);
|
||||||
|
rowStartDate.setDate(gridStartDate.getDate() + (index * 7));
|
||||||
|
let thursday = new Date(rowStartDate);
|
||||||
|
if (firstDayOfWeek === 0) {
|
||||||
|
thursday.setDate(rowStartDate.getDate() + 4);
|
||||||
|
} else if (firstDayOfWeek === 1) {
|
||||||
|
thursday.setDate(rowStartDate.getDate() + 3);
|
||||||
|
} else {
|
||||||
|
let daysToThursday = (4 - firstDayOfWeek + 7) % 7;
|
||||||
|
thursday.setDate(rowStartDate.getDate() + daysToThursday);
|
||||||
|
}
|
||||||
|
return `${getISOWeekNumber(thursday)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Days Grid
|
||||||
|
MonthGrid {
|
||||||
|
id: grid
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
spacing: Style.marginXXS
|
||||||
|
month: Time.date.getMonth()
|
||||||
|
year: Time.date.getFullYear()
|
||||||
|
locale: Qt.locale()
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
Rectangle {
|
||||||
|
width: Style.baseWidgetSize * 0.9
|
||||||
|
height: Style.baseWidgetSize * 0.9
|
||||||
|
anchors.centerIn: parent
|
||||||
|
radius: Style.radiusM
|
||||||
|
color: model.today ? Color.mSecondary : Color.transparent
|
||||||
|
|
||||||
|
NText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: model.day
|
||||||
|
color: {
|
||||||
|
if (model.today)
|
||||||
|
return Color.mOnSecondary;
|
||||||
|
|
||||||
|
if (model.month === grid.month)
|
||||||
|
return Color.mOnSurface;
|
||||||
|
|
||||||
|
return Color.mOnSurfaceVariant;
|
||||||
|
}
|
||||||
|
opacity: model.month === grid.month ? 1 : 0.4
|
||||||
|
pointSize: Style.fontSizeM
|
||||||
|
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
quickshell/Modules/Panel/Cards/LyricsCard.qml
Normal file
46
quickshell/Modules/Panel/Cards/LyricsCard.qml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
NBox {
|
||||||
|
id: lyricsBox
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
LyricsService.startSyncing();
|
||||||
|
}
|
||||||
|
Component.onDestruction: {
|
||||||
|
LyricsService.stopSyncing();
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: lyricsColumn
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: LyricsService.lyrics
|
||||||
|
|
||||||
|
NText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: modelData
|
||||||
|
font.pointSize: index === LyricsService.currentIndex ? Style.fontSizeM : Style.fontSizeS
|
||||||
|
font.weight: index === LyricsService.currentIndex ? Style.fontWeightBold : Style.fontWeightRegular
|
||||||
|
font.family: Fonts.sans
|
||||||
|
color: index === LyricsService.currentIndex ? Color.mOnSurface : Color.mOnSurfaceVariant
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
96
quickshell/Modules/Panel/Cards/LyricsControl.qml
Normal file
96
quickshell/Modules/Panel/Cards/LyricsControl.qml
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Modules.Bar.Misc
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
id: buttonsGrid
|
||||||
|
|
||||||
|
columns: 2
|
||||||
|
columnSpacing: 10
|
||||||
|
rowSpacing: 10
|
||||||
|
Layout.margins: 10
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: slowerButton
|
||||||
|
|
||||||
|
baseSize: 32
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorBgHover: Colors.blue
|
||||||
|
colorFg: Colors.blue
|
||||||
|
icon: "arrow-bar-up"
|
||||||
|
onClicked: {
|
||||||
|
LyricsService.increaseOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: playPauseButton
|
||||||
|
|
||||||
|
baseSize: 32
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorBgHover: Colors.yellow
|
||||||
|
colorFg: Colors.yellow
|
||||||
|
icon: "arrow-bar-down"
|
||||||
|
onClicked: {
|
||||||
|
LyricsService.decreaseOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: nextButton
|
||||||
|
|
||||||
|
baseSize: 32
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorBgHover: Colors.green
|
||||||
|
colorFg: Colors.green
|
||||||
|
icon: "rotate-clockwise"
|
||||||
|
onClicked: {
|
||||||
|
LyricsService.resetOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: fasterButton
|
||||||
|
|
||||||
|
baseSize: 32
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorBgHover: Colors.red
|
||||||
|
colorFg: Colors.red
|
||||||
|
icon: "trash"
|
||||||
|
onClicked: {
|
||||||
|
LyricsService.clearCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: barLyricsButton
|
||||||
|
|
||||||
|
baseSize: 32
|
||||||
|
colorBg: SettingsService.showLyricsBar ? Colors.peach : Color.transparent
|
||||||
|
colorBgHover: Colors.peach
|
||||||
|
colorFg: SettingsService.showLyricsBar ? Colors.base : Colors.peach
|
||||||
|
icon: "app-window"
|
||||||
|
onClicked: {
|
||||||
|
SettingsService.showLyricsBar = !SettingsService.showLyricsBar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: textButton
|
||||||
|
|
||||||
|
baseSize: 32
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorBgHover: Colors.subtext1
|
||||||
|
colorFg: Colors.subtext1
|
||||||
|
icon: "align-box-left-bottom"
|
||||||
|
onClicked: {
|
||||||
|
LyricsService.showLyricsText();
|
||||||
|
controlCenterPanel.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
458
quickshell/Modules/Panel/Cards/MediaCard.qml
Normal file
458
quickshell/Modules/Panel/Cards/MediaCard.qml
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
NBox {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Background artwork that covers everything
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
NImageRounded {
|
||||||
|
id: bgArtImage
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
imagePath: MusicManager.trackArtUrl
|
||||||
|
imageRadius: Style.radiusM
|
||||||
|
visible: MusicManager.trackArtUrl !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark overlay for readability
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
opacity: 0.85
|
||||||
|
radius: Style.radiusM
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Color.transparent
|
||||||
|
radius: Style.radiusM
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background visualizer on top of the artwork
|
||||||
|
Item {
|
||||||
|
id: visualizerContainer
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
layer.enabled: true
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Cava {
|
||||||
|
id: cava
|
||||||
|
|
||||||
|
count: 32
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: cava.values
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: (parent.width - (cava.count - 1) * Style.marginXS) / cava.count
|
||||||
|
height: modelData * parent.height
|
||||||
|
x: index * (width + Style.marginXS)
|
||||||
|
color: Color.mPrimary
|
||||||
|
radius: width / 2
|
||||||
|
opacity: 0.25
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
maskEnabled: true
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 0
|
||||||
|
|
||||||
|
maskSource: ShaderEffectSource {
|
||||||
|
|
||||||
|
sourceItem: Rectangle {
|
||||||
|
width: root.width
|
||||||
|
height: root.height
|
||||||
|
radius: Style.radiusM
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player selector - positioned at the very top
|
||||||
|
Rectangle {
|
||||||
|
id: playerSelectorButton
|
||||||
|
|
||||||
|
property var currentPlayer: MusicManager.getAvailablePlayers()[MusicManager.selectedPlayerIndex]
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: Style.marginXS
|
||||||
|
anchors.leftMargin: Style.marginM
|
||||||
|
anchors.rightMargin: Style.marginM
|
||||||
|
height: Style.barHeight
|
||||||
|
visible: MusicManager.getAvailablePlayers().length > 1
|
||||||
|
radius: Style.radiusM
|
||||||
|
color: Color.transparent
|
||||||
|
Component.onCompleted: {
|
||||||
|
MusicManager.selectedPlayerIndex = -1;
|
||||||
|
}
|
||||||
|
Component.onDestruction: {
|
||||||
|
MusicManager.selectedPlayerIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: "caret-down"
|
||||||
|
pointSize: Style.fontSizeXXL
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: playerSelectorButton.currentPlayer ? playerSelectorButton.currentPlayer.identity : ""
|
||||||
|
pointSize: Style.fontSizeXS
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: playerSelectorMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
var menuItems = [];
|
||||||
|
var players = MusicManager.getAvailablePlayers();
|
||||||
|
for (var i = 0; i < players.length; i++) {
|
||||||
|
menuItems.push({
|
||||||
|
"label": players[i].identity,
|
||||||
|
"action": i.toString(),
|
||||||
|
"icon": "disc",
|
||||||
|
"enabled": true,
|
||||||
|
"visible": true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
playerContextMenu.model = menuItems;
|
||||||
|
playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NContextMenu {
|
||||||
|
id: playerContextMenu
|
||||||
|
|
||||||
|
parent: root
|
||||||
|
width: 200
|
||||||
|
onTriggered: function(action) {
|
||||||
|
var index = parseInt(action);
|
||||||
|
if (!isNaN(index)) {
|
||||||
|
MusicManager.selectedPlayerIndex = index;
|
||||||
|
MusicManager.updateCurrentPlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginM
|
||||||
|
|
||||||
|
// No media player detected
|
||||||
|
ColumnLayout {
|
||||||
|
id: fallback
|
||||||
|
|
||||||
|
visible: !main.visible
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Style.marginL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.preferredWidth: Style.fontSizeXXXL * 4
|
||||||
|
Layout.preferredHeight: Style.fontSizeXXXL * 4
|
||||||
|
|
||||||
|
// Pulsating audio circles (background)
|
||||||
|
Repeater {
|
||||||
|
model: 3
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width * (1 + index * 0.2)
|
||||||
|
height: width
|
||||||
|
radius: width / 2
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Color.mOnSurfaceVariant
|
||||||
|
border.width: 2
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
SequentialAnimation on opacity {
|
||||||
|
running: true
|
||||||
|
loops: Animation.Infinite
|
||||||
|
|
||||||
|
PauseAnimation {
|
||||||
|
duration: index * 600
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
from: 1
|
||||||
|
to: 0
|
||||||
|
duration: 2000
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation on scale {
|
||||||
|
running: true
|
||||||
|
loops: Animation.Infinite
|
||||||
|
|
||||||
|
PauseAnimation {
|
||||||
|
duration: index * 600
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
from: 0.5
|
||||||
|
to: 1.2
|
||||||
|
duration: 2000
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spinning disc
|
||||||
|
NIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
icon: "disc"
|
||||||
|
pointSize: Style.fontSizeXXXL * 3
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
|
||||||
|
RotationAnimator on rotation {
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 8000
|
||||||
|
loops: Animation.Infinite
|
||||||
|
running: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptive text
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
spacing: Style.marginXS
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaPlayer Main Content
|
||||||
|
ColumnLayout {
|
||||||
|
id: main
|
||||||
|
|
||||||
|
visible: MusicManager.currentPlayer && MusicManager.canPlay
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
// Spacer to push content down
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: Style.marginM
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata at the bottom left
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
spacing: Style.marginXS
|
||||||
|
|
||||||
|
NText {
|
||||||
|
visible: MusicManager.trackTitle !== ""
|
||||||
|
text: MusicManager.trackTitle
|
||||||
|
pointSize: Style.fontSizeM
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
visible: MusicManager.trackArtist !== ""
|
||||||
|
text: MusicManager.trackArtist
|
||||||
|
color: Color.mPrimary
|
||||||
|
pointSize: Style.fontSizeS
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
visible: MusicManager.trackAlbum !== ""
|
||||||
|
text: MusicManager.trackAlbum
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
pointSize: Style.fontSizeM
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress slider
|
||||||
|
Item {
|
||||||
|
id: progressWrapper
|
||||||
|
|
||||||
|
property real localSeekRatio: -1
|
||||||
|
property real lastSentSeekRatio: -1
|
||||||
|
property real seekEpsilon: 0.01
|
||||||
|
property real progressRatio: {
|
||||||
|
if (!MusicManager.currentPlayer || MusicManager.trackLength <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const r = MusicManager.currentPosition / MusicManager.trackLength;
|
||||||
|
if (isNaN(r) || !isFinite(r))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return Math.max(0, Math.min(1, r));
|
||||||
|
}
|
||||||
|
property real effectiveRatio: (MusicManager.isSeeking && localSeekRatio >= 0) ? Math.max(0, Math.min(1, localSeekRatio)) : progressRatio
|
||||||
|
|
||||||
|
visible: (MusicManager.currentPlayer && MusicManager.trackLength > 0)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: Style.baseWidgetSize * 0.5
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: seekDebounce
|
||||||
|
|
||||||
|
interval: 75
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
if (MusicManager.isSeeking && progressWrapper.localSeekRatio >= 0) {
|
||||||
|
const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio));
|
||||||
|
if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
|
||||||
|
MusicManager.seekByRatio(next);
|
||||||
|
progressWrapper.lastSentSeekRatio = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSlider {
|
||||||
|
id: progressSlider
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
stepSize: 0
|
||||||
|
snapAlways: false
|
||||||
|
enabled: MusicManager.trackLength > 0 && MusicManager.canSeek
|
||||||
|
heightRatio: 0.65
|
||||||
|
onMoved: {
|
||||||
|
progressWrapper.localSeekRatio = value;
|
||||||
|
seekDebounce.restart();
|
||||||
|
}
|
||||||
|
onPressedChanged: {
|
||||||
|
if (pressed) {
|
||||||
|
MusicManager.isSeeking = true;
|
||||||
|
progressWrapper.localSeekRatio = value;
|
||||||
|
MusicManager.seekByRatio(value);
|
||||||
|
progressWrapper.lastSentSeekRatio = value;
|
||||||
|
} else {
|
||||||
|
seekDebounce.stop();
|
||||||
|
MusicManager.seekByRatio(value);
|
||||||
|
MusicManager.isSeeking = false;
|
||||||
|
progressWrapper.localSeekRatio = -1;
|
||||||
|
progressWrapper.lastSentSeekRatio = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: progressSlider
|
||||||
|
property: "value"
|
||||||
|
value: progressWrapper.progressRatio
|
||||||
|
when: !MusicManager.isSeeking
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media controls
|
||||||
|
RowLayout {
|
||||||
|
spacing: Style.marginS
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "media-prev"
|
||||||
|
visible: MusicManager.canGoPrevious
|
||||||
|
onClicked: MusicManager.canGoPrevious ? MusicManager.previous() : {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: MusicManager.isPlaying ? "media-pause" : "media-play"
|
||||||
|
visible: (MusicManager.canPlay || MusicManager.canPause)
|
||||||
|
onClicked: (MusicManager.canPlay || MusicManager.canPause) ? MusicManager.playPause() : {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "media-next"
|
||||||
|
visible: MusicManager.canGoNext
|
||||||
|
onClicked: MusicManager.canGoNext ? MusicManager.next() : {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
61
quickshell/Modules/Panel/Cards/SystemMonitorCard.qml
Normal file
61
quickshell/Modules/Panel/Cards/SystemMonitorCard.qml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Modules.Panel.Misc
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
// Unified system card: monitors CPU, temp, memory, disk
|
||||||
|
NBox {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
compact: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginXS
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
MonitorSlider {
|
||||||
|
icon: "cpu-usage"
|
||||||
|
value: SystemStatService.cpuUsage
|
||||||
|
from: 0
|
||||||
|
to: 100
|
||||||
|
colorFill: Colors.teal
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorSlider {
|
||||||
|
icon: "memory"
|
||||||
|
value: SystemStatService.memPercent
|
||||||
|
from: 0
|
||||||
|
to: 100
|
||||||
|
colorFill: Colors.green
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorSlider {
|
||||||
|
icon: "cpu-temperature"
|
||||||
|
value: SystemStatService.cpuTemp
|
||||||
|
from: 0
|
||||||
|
to: 100
|
||||||
|
colorFill: Colors.yellow
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorSlider {
|
||||||
|
icon: "storage"
|
||||||
|
value: SystemStatService.diskPercent
|
||||||
|
from: 0
|
||||||
|
to: 100
|
||||||
|
colorFill: Colors.peach
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
175
quickshell/Modules/Panel/Cards/TopLeftCard.qml
Normal file
175
quickshell/Modules/Panel/Cards/TopLeftCard.qml
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool hasPP: PowerProfileService.available
|
||||||
|
|
||||||
|
spacing: Style.marginM
|
||||||
|
|
||||||
|
NBox {
|
||||||
|
id: whoamiBox
|
||||||
|
|
||||||
|
property string uptimeText: "--"
|
||||||
|
property string hostname: "--"
|
||||||
|
|
||||||
|
function updateSystemInfo() {
|
||||||
|
uptimeProcess.running = true;
|
||||||
|
hostnameProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
spacing: root.spacing
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: root.spacing
|
||||||
|
|
||||||
|
NImageCircled {
|
||||||
|
width: Style.baseWidgetSize * 1.5
|
||||||
|
height: Style.baseWidgetSize * 1.5
|
||||||
|
imagePath: Quickshell.shellDir + "/Assets/Images/Avatar.jpg"
|
||||||
|
fallbackIcon: "person"
|
||||||
|
borderColor: Color.mPrimary
|
||||||
|
borderWidth: Math.max(1, Style.borderM)
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.topMargin: Style.marginXS
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginXXS
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: `${Quickshell.env("USER") || "user"} @ ${whoamiBox.hostname}`
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
font.pointSize: Style.fontSizeL
|
||||||
|
font.capitalization: Font.Capitalize
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Uptime: " + whoamiBox.uptimeText
|
||||||
|
font.pointSize: Style.fontSizeM
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// Uptime
|
||||||
|
Timer {
|
||||||
|
interval: 60000
|
||||||
|
repeat: true
|
||||||
|
running: true
|
||||||
|
onTriggered: uptimeProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: uptimeProcess
|
||||||
|
|
||||||
|
command: ["cat", "/proc/uptime"]
|
||||||
|
running: true
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]);
|
||||||
|
whoamiBox.uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds);
|
||||||
|
uptimeProcess.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: hostnameProcess
|
||||||
|
|
||||||
|
command: ["cat", "/etc/hostname"]
|
||||||
|
running: true
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
whoamiBox.hostname = this.text.trim();
|
||||||
|
hostnameProcess.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: utilitiesRow
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
// Performance
|
||||||
|
NIconButton {
|
||||||
|
implicitHeight: 32
|
||||||
|
implicitWidth: 32
|
||||||
|
icon: PowerProfileService.getIcon(PowerProfile.Performance)
|
||||||
|
enabled: hasPP
|
||||||
|
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||||
|
colorBgHover: Colors.red
|
||||||
|
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Colors.red : Color.transparent
|
||||||
|
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Colors.red
|
||||||
|
onClicked: PowerProfileService.setProfile(PowerProfile.Performance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Balanced
|
||||||
|
NIconButton {
|
||||||
|
implicitHeight: 32
|
||||||
|
implicitWidth: 32
|
||||||
|
icon: PowerProfileService.getIcon(PowerProfile.Balanced)
|
||||||
|
enabled: hasPP
|
||||||
|
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||||
|
colorBgHover: Colors.blue
|
||||||
|
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Colors.blue : Color.transparent
|
||||||
|
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Colors.blue
|
||||||
|
onClicked: PowerProfileService.setProfile(PowerProfile.Balanced)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eco
|
||||||
|
NIconButton {
|
||||||
|
implicitHeight: 32
|
||||||
|
implicitWidth: 32
|
||||||
|
icon: PowerProfileService.getIcon(PowerProfile.PowerSaver)
|
||||||
|
enabled: hasPP
|
||||||
|
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||||
|
colorBgHover: Colors.green
|
||||||
|
colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Colors.green : Color.transparent
|
||||||
|
colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Colors.green
|
||||||
|
onClicked: PowerProfileService.setProfile(PowerProfile.PowerSaver)
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lyrics Offset
|
||||||
|
NText {
|
||||||
|
text: `Lyrics Offset: ${LyricsService.offset >= 0 ? '+' : ''}${LyricsService.offset} ms`
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
86
quickshell/Modules/Panel/ControlCenterPanel.qml
Normal file
86
quickshell/Modules/Panel/ControlCenterPanel.qml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Modules.Panel.Cards
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
NPanel {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Positioning
|
||||||
|
readonly property string controlCenterPosition: "top_left"
|
||||||
|
property real topCardHeight: 120
|
||||||
|
property real middleCardHeight: 100
|
||||||
|
property real bottomCardHeight: 200
|
||||||
|
|
||||||
|
preferredWidth: 480
|
||||||
|
preferredHeight: topCardHeight + middleCardHeight + bottomCardHeight + Style.marginL * 4
|
||||||
|
panelKeyboardFocus: false
|
||||||
|
panelAnchorHorizontalCenter: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_center")
|
||||||
|
panelAnchorVerticalCenter: false
|
||||||
|
panelAnchorLeft: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_left")
|
||||||
|
panelAnchorRight: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_right")
|
||||||
|
panelAnchorBottom: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("bottom_")
|
||||||
|
panelAnchorTop: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("top_")
|
||||||
|
|
||||||
|
panelContent: Item {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
property real cardSpacing: Style.marginL
|
||||||
|
|
||||||
|
// Layout content
|
||||||
|
ColumnLayout {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: content.cardSpacing
|
||||||
|
spacing: content.cardSpacing
|
||||||
|
|
||||||
|
// Top Card: profile + utilities
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: topCardHeight
|
||||||
|
|
||||||
|
TopLeftCard {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumHeight: topCardHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
LyricsControl {
|
||||||
|
Layout.preferredHeight: topCardHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
LyricsCard {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: middleCardHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media + stats column
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: bottomCardHeight
|
||||||
|
spacing: content.cardSpacing
|
||||||
|
|
||||||
|
SystemMonitorCard {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: bottomCardHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaCard {
|
||||||
|
Layout.preferredWidth: 270
|
||||||
|
Layout.preferredHeight: bottomCardHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
54
quickshell/Modules/Panel/Misc/MonitorSlider.qml
Normal file
54
quickshell/Modules/Panel/Misc/MonitorSlider.qml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string icon: "volume-high"
|
||||||
|
property real value: 50
|
||||||
|
property real from: 0
|
||||||
|
property real to: 100
|
||||||
|
property color colorFill: Colors.primary
|
||||||
|
property color colorRest: Colors.surface0
|
||||||
|
|
||||||
|
implicitHeight: layout.implicitHeight
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
id: iconItem
|
||||||
|
|
||||||
|
icon: root.icon
|
||||||
|
color: root.colorFill
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: whole
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: root.colorRest
|
||||||
|
radius: height / 2
|
||||||
|
height: Style.baseWidgetSize * 0.3
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: fill
|
||||||
|
|
||||||
|
width: Math.max(0, Math.min(whole.width, (root.value - root.from) / (root.to - root.from) * whole.width))
|
||||||
|
height: parent.height
|
||||||
|
color: root.colorFill
|
||||||
|
radius: height / 2
|
||||||
|
anchors.left: parent.left
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
28
quickshell/Noctalia/NBox.qml
Normal file
28
quickshell/Noctalia/NBox.qml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
|
||||||
|
// Rounded group container using the variant surface color.
|
||||||
|
// To be used in side panels and settings panes to group fields or buttons.
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool compact: false
|
||||||
|
|
||||||
|
implicitWidth: childrenRect.width
|
||||||
|
implicitHeight: childrenRect.height
|
||||||
|
color: compact ? Color.transparent : Color.mSurfaceVariant
|
||||||
|
radius: Style.radiusM
|
||||||
|
layer.enabled: !compact
|
||||||
|
|
||||||
|
layer.effect: DropShadow {
|
||||||
|
horizontalOffset: 6
|
||||||
|
verticalOffset: 6
|
||||||
|
radius: 8
|
||||||
|
samples: 12
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
53
quickshell/Noctalia/NBusyIndicator.qml
Normal file
53
quickshell/Noctalia/NBusyIndicator.qml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool running: true
|
||||||
|
property color color: Color.mPrimary
|
||||||
|
property int size: Style.baseWidgetSize
|
||||||
|
property int strokeWidth: Style.borderL
|
||||||
|
property int duration: Style.animationSlow * 2
|
||||||
|
|
||||||
|
implicitWidth: size
|
||||||
|
implicitHeight: size
|
||||||
|
|
||||||
|
Canvas {
|
||||||
|
id: canvas
|
||||||
|
|
||||||
|
property real rotationAngle: 0
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
onPaint: {
|
||||||
|
var ctx = getContext("2d");
|
||||||
|
ctx.reset();
|
||||||
|
var centerX = width / 2;
|
||||||
|
var centerY = height / 2;
|
||||||
|
var radius = Math.min(width, height) / 2 - strokeWidth / 2;
|
||||||
|
ctx.strokeStyle = root.color;
|
||||||
|
ctx.lineWidth = Math.max(1, root.strokeWidth);
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
// Draw arc with gap (270 degrees with 90 degree gap)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, radius, -Math.PI / 2 + rotationAngle, -Math.PI / 2 + rotationAngle + Math.PI * 1.5);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
onRotationAngleChanged: {
|
||||||
|
requestPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
target: canvas
|
||||||
|
property: "rotationAngle"
|
||||||
|
running: root.running
|
||||||
|
from: 0
|
||||||
|
to: 2 * Math.PI
|
||||||
|
duration: root.duration
|
||||||
|
loops: Animation.Infinite
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
122
quickshell/Noctalia/NCircleStat.qml
Normal file
122
quickshell/Noctalia/NCircleStat.qml
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
// Compact circular statistic display using Layout management
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property real value: 0 // 0..100 (or any range visually mapped)
|
||||||
|
property string icon: ""
|
||||||
|
property string suffix: "%"
|
||||||
|
// When nested inside a parent group (NBox), you can make it flat
|
||||||
|
property bool flat: false
|
||||||
|
// Scales the internal content (labels, gauge, icon) without changing the
|
||||||
|
// outer width/height footprint of the component
|
||||||
|
property real contentScale: 1
|
||||||
|
|
||||||
|
width: 68
|
||||||
|
height: 92
|
||||||
|
color: flat ? Color.transparent : Color.mSurface
|
||||||
|
radius: Style.radiusS
|
||||||
|
border.color: flat ? Color.transparent : Color.mSurfaceVariant
|
||||||
|
border.width: flat ? 0 : Math.max(1, Style.borderS)
|
||||||
|
// Repaint gauge when the bound value changes
|
||||||
|
onValueChanged: gauge.requestPaint()
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: mainLayout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginS * contentScale
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// Main gauge container
|
||||||
|
Item {
|
||||||
|
id: gaugeContainer
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
Layout.preferredWidth: 68 * contentScale
|
||||||
|
Layout.preferredHeight: 68 * contentScale
|
||||||
|
|
||||||
|
Canvas {
|
||||||
|
// 390° (equivalent to 30°)
|
||||||
|
|
||||||
|
id: gauge
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
renderStrategy: Canvas.Cooperative
|
||||||
|
onPaint: {
|
||||||
|
const ctx = getContext("2d");
|
||||||
|
const w = width, h = height;
|
||||||
|
const cx = w / 2, cy = h / 2;
|
||||||
|
const r = Math.min(w, h) / 2 - 5 * contentScale;
|
||||||
|
// Rotated 90° to the right: gap at the bottom
|
||||||
|
// Start at 150° and end at 390° (30°) → bottom opening
|
||||||
|
const start = Math.PI * 5 / 6;
|
||||||
|
// 150°
|
||||||
|
const endBg = Math.PI * 13 / 6;
|
||||||
|
ctx.reset();
|
||||||
|
ctx.lineWidth = 6 * contentScale;
|
||||||
|
// Track uses surfaceVariant for stronger contrast
|
||||||
|
ctx.strokeStyle = Color.mSurface;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, r, start, endBg);
|
||||||
|
ctx.stroke();
|
||||||
|
// Value arc
|
||||||
|
const ratio = Math.max(0, Math.min(1, root.value / 100));
|
||||||
|
const end = start + (endBg - start) * ratio;
|
||||||
|
ctx.strokeStyle = Color.mPrimary;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, r, start, end);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Percent centered in the circle
|
||||||
|
NText {
|
||||||
|
id: valueLabel
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
anchors.verticalCenterOffset: -4 * contentScale
|
||||||
|
text: `${root.value}${root.suffix}`
|
||||||
|
pointSize: Style.fontSizeM * contentScale
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurface
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tiny circular badge for the icon, positioned inside below the percentage
|
||||||
|
Rectangle {
|
||||||
|
id: iconBadge
|
||||||
|
|
||||||
|
width: iconText.implicitWidth + Style.marginXXS
|
||||||
|
height: width
|
||||||
|
radius: width / 2
|
||||||
|
color: Color.mPrimary
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: valueLabel.bottom
|
||||||
|
anchors.topMargin: 8 * contentScale
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
id: iconText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
icon: root.icon
|
||||||
|
color: Color.mOnPrimary
|
||||||
|
pointSize: Style.fontSizeS
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
124
quickshell/Noctalia/NContextMenu.qml
Normal file
124
quickshell/Noctalia/NContextMenu.qml
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias model: listView.model
|
||||||
|
property real itemHeight: 36
|
||||||
|
property real itemPadding: Style.marginM
|
||||||
|
|
||||||
|
signal triggered(string action)
|
||||||
|
|
||||||
|
// Helper function to open at mouse position
|
||||||
|
function openAt(x, y) {
|
||||||
|
root.x = x;
|
||||||
|
root.y = y;
|
||||||
|
root.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to open at item
|
||||||
|
function openAtItem(item, mouseX, mouseY) {
|
||||||
|
var pos = item.mapToItem(root.parent, mouseX || 0, mouseY || 0);
|
||||||
|
openAt(pos.x, pos.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
width: 180
|
||||||
|
padding: Style.marginS
|
||||||
|
onOpened: PanelService.willOpenPopup(root)
|
||||||
|
onClosed: PanelService.willClosePopup(root)
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Math.max(1, Style.borderS)
|
||||||
|
radius: Style.radiusM
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: NListView {
|
||||||
|
id: listView
|
||||||
|
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
spacing: Style.marginXXS
|
||||||
|
interactive: contentHeight > root.height
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
id: menuItem
|
||||||
|
|
||||||
|
// Store reference to the popup
|
||||||
|
property var popup: root
|
||||||
|
|
||||||
|
width: listView.width
|
||||||
|
height: modelData.visible !== false ? root.itemHeight : 0
|
||||||
|
visible: modelData.visible !== false
|
||||||
|
opacity: modelData.enabled !== false ? 1 : 0.5
|
||||||
|
enabled: modelData.enabled !== false
|
||||||
|
onClicked: {
|
||||||
|
if (enabled) {
|
||||||
|
popup.triggered(modelData.action || modelData.key || index.toString());
|
||||||
|
popup.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: menuItem.hovered && menuItem.enabled ? Color.mTertiary : Color.transparent
|
||||||
|
radius: Style.radiusS
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
// Optional icon
|
||||||
|
NIcon {
|
||||||
|
visible: modelData.icon !== undefined
|
||||||
|
icon: modelData.icon || ""
|
||||||
|
pointSize: Style.fontSizeM
|
||||||
|
color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface
|
||||||
|
Layout.leftMargin: root.itemPadding
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: modelData.label || modelData.text || ""
|
||||||
|
pointSize: Style.fontSizeM
|
||||||
|
color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: modelData.icon === undefined ? root.itemPadding : 0
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
35
quickshell/Noctalia/NDivider.qml
Normal file
35
quickshell/Noctalia/NDivider.qml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Constants
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: Math.max(1, Style.borderS)
|
||||||
|
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Horizontal
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
position: 0
|
||||||
|
color: Color.transparent
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
position: 0.1
|
||||||
|
color: Color.mOutline
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
position: 0.9
|
||||||
|
color: Color.mOutline
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
position: 1
|
||||||
|
color: Color.transparent
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
28
quickshell/Noctalia/NIcon.qml
Normal file
28
quickshell/Noctalia/NIcon.qml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string icon: Icons.defaultIcon
|
||||||
|
property real pointSize: Style.fontSizeL
|
||||||
|
|
||||||
|
visible: (icon !== undefined) && (icon !== "")
|
||||||
|
text: {
|
||||||
|
if ((icon === undefined) || (icon === ""))
|
||||||
|
return "";
|
||||||
|
|
||||||
|
if (Icons.get(icon) === undefined) {
|
||||||
|
Logger.warn("Icon", `"${icon}"`, "doesn't exist in the icons font");
|
||||||
|
Logger.callStack();
|
||||||
|
return Icons.get(Icons.defaultIcon);
|
||||||
|
}
|
||||||
|
return Icons.get(icon);
|
||||||
|
}
|
||||||
|
font.family: Icons.fontFamily
|
||||||
|
font.pointSize: root.pointSize
|
||||||
|
color: Color.mOnSurface
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
92
quickshell/Noctalia/NIconButton.qml
Normal file
92
quickshell/Noctalia/NIconButton.qml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property real baseSize: Style.baseWidgetSize
|
||||||
|
property string icon
|
||||||
|
property bool enabled: true
|
||||||
|
property bool allowClickWhenDisabled: false
|
||||||
|
property bool hovering: false
|
||||||
|
property bool compact: false
|
||||||
|
property color colorBg: Color.mSurfaceVariant
|
||||||
|
property color colorFg: Color.mPrimary
|
||||||
|
property color colorBgHover: Color.mTertiary
|
||||||
|
property color colorFgHover: Color.mOnTertiary
|
||||||
|
property color colorBorder: Color.transparent
|
||||||
|
property color colorBorderHover: Color.transparent
|
||||||
|
|
||||||
|
signal entered()
|
||||||
|
signal exited()
|
||||||
|
signal clicked()
|
||||||
|
signal rightClicked()
|
||||||
|
signal middleClicked()
|
||||||
|
|
||||||
|
implicitWidth: Math.round(baseSize)
|
||||||
|
implicitHeight: Math.round(baseSize)
|
||||||
|
opacity: root.enabled ? Style.opacityFull : Style.opacityMedium
|
||||||
|
color: root.enabled && root.hovering ? colorBgHover : colorBg
|
||||||
|
radius: width * 0.5
|
||||||
|
border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder
|
||||||
|
border.width: Math.max(1, Style.borderS)
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: root.icon
|
||||||
|
pointSize: Math.max(1, root.compact ? root.width * 0.65 : root.width * 0.48)
|
||||||
|
color: root.enabled && root.hovering ? colorFgHover : colorFg
|
||||||
|
// Center horizontally
|
||||||
|
x: (root.width - width) / 2
|
||||||
|
// Center vertically accounting for font metrics
|
||||||
|
y: (root.height - height) / 2 + (height - contentHeight) / 2
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
// Always enabled to allow hover/tooltip even when the button is disabled
|
||||||
|
enabled: true
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
hoverEnabled: true
|
||||||
|
onEntered: {
|
||||||
|
hovering = root.enabled ? true : false;
|
||||||
|
root.entered();
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
hovering = false;
|
||||||
|
root.exited();
|
||||||
|
}
|
||||||
|
onClicked: function(mouse) {
|
||||||
|
if (!root.enabled && !allowClickWhenDisabled)
|
||||||
|
return ;
|
||||||
|
|
||||||
|
if (mouse.button === Qt.LeftButton)
|
||||||
|
root.clicked();
|
||||||
|
else if (mouse.button === Qt.RightButton)
|
||||||
|
root.rightClicked();
|
||||||
|
else if (mouse.button === Qt.MiddleButton)
|
||||||
|
root.middleClicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
85
quickshell/Noctalia/NImageCircled.qml
Normal file
85
quickshell/Noctalia/NImageCircled.qml
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string imagePath: ""
|
||||||
|
property color borderColor: Color.transparent
|
||||||
|
property real borderWidth: 0
|
||||||
|
property string fallbackIcon: ""
|
||||||
|
property real fallbackIconSize: Style.fontSizeXXL
|
||||||
|
|
||||||
|
color: Color.transparent
|
||||||
|
radius: parent.width * 0.5
|
||||||
|
anchors.margins: Style.marginXXS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
color: Color.transparent
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: img
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: imagePath
|
||||||
|
visible: false // Hide since we're using it as shader source
|
||||||
|
mipmap: true
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
antialiasing: true
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderEffect {
|
||||||
|
property var source
|
||||||
|
property real imageOpacity: root.opacity
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/circled_image.frag.qsb")
|
||||||
|
supportsAtlasTextures: false
|
||||||
|
blending: true
|
||||||
|
|
||||||
|
source: ShaderEffectSource {
|
||||||
|
sourceItem: img
|
||||||
|
hideSource: true
|
||||||
|
live: true
|
||||||
|
recursive: false
|
||||||
|
format: ShaderEffectSource.RGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback icon
|
||||||
|
Loader {
|
||||||
|
active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
sourceComponent: NIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
icon: fallbackIcon
|
||||||
|
pointSize: fallbackIconSize
|
||||||
|
z: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: parent.radius
|
||||||
|
color: Color.transparent
|
||||||
|
border.color: parent.borderColor
|
||||||
|
border.width: parent.borderWidth
|
||||||
|
antialiasing: true
|
||||||
|
z: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
103
quickshell/Noctalia/NImageRounded.qml
Normal file
103
quickshell/Noctalia/NImageRounded.qml
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string imagePath: ""
|
||||||
|
property color borderColor: Color.transparent
|
||||||
|
property real borderWidth: 0
|
||||||
|
property real imageRadius: width * 0.5
|
||||||
|
property string fallbackIcon: ""
|
||||||
|
property real fallbackIconSize: Style.fontSizeXXL
|
||||||
|
property real scaledRadius: imageRadius
|
||||||
|
|
||||||
|
signal statusChanged(int status)
|
||||||
|
|
||||||
|
color: Color.transparent
|
||||||
|
radius: scaledRadius
|
||||||
|
anchors.margins: Style.marginXXS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
color: Color.transparent
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: img
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: imagePath
|
||||||
|
visible: false // Hide since we're using it as shader source
|
||||||
|
mipmap: true
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
antialiasing: true
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
onStatusChanged: root.statusChanged(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderEffect {
|
||||||
|
property var source
|
||||||
|
// Use custom property names to avoid conflicts with final properties
|
||||||
|
property real itemWidth: root.width
|
||||||
|
property real itemHeight: root.height
|
||||||
|
property real cornerRadius: root.radius
|
||||||
|
property real imageOpacity: root.opacity
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
|
||||||
|
// Qt6 specific properties - ensure proper blending
|
||||||
|
supportsAtlasTextures: false
|
||||||
|
blending: true
|
||||||
|
|
||||||
|
// Make sure the background is transparent
|
||||||
|
Rectangle {
|
||||||
|
id: background
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Color.transparent
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
source: ShaderEffectSource {
|
||||||
|
sourceItem: img
|
||||||
|
hideSource: true
|
||||||
|
live: true
|
||||||
|
recursive: false
|
||||||
|
format: ShaderEffectSource.RGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback icon
|
||||||
|
Loader {
|
||||||
|
active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
sourceComponent: NIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
icon: fallbackIcon
|
||||||
|
pointSize: fallbackIconSize
|
||||||
|
z: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: parent.radius
|
||||||
|
color: Color.transparent
|
||||||
|
border.color: parent.borderColor
|
||||||
|
border.width: parent.borderWidth
|
||||||
|
antialiasing: true
|
||||||
|
z: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
217
quickshell/Noctalia/NListView.qml
Normal file
217
quickshell/Noctalia/NListView.qml
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Templates as T
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property color handleColor: Qt.alpha(Color.mTertiary, 0.8)
|
||||||
|
property color handleHoverColor: handleColor
|
||||||
|
property color handlePressedColor: handleColor
|
||||||
|
property color trackColor: Color.transparent
|
||||||
|
property real handleWidth: 6
|
||||||
|
property real handleRadius: Style.radiusM
|
||||||
|
property int verticalPolicy: ScrollBar.AsNeeded
|
||||||
|
property int horizontalPolicy: ScrollBar.AsNeeded
|
||||||
|
// Forward ListView properties
|
||||||
|
property alias model: listView.model
|
||||||
|
property alias delegate: listView.delegate
|
||||||
|
property alias spacing: listView.spacing
|
||||||
|
property alias orientation: listView.orientation
|
||||||
|
property alias currentIndex: listView.currentIndex
|
||||||
|
property alias count: listView.count
|
||||||
|
property alias contentHeight: listView.contentHeight
|
||||||
|
property alias contentWidth: listView.contentWidth
|
||||||
|
property alias contentY: listView.contentY
|
||||||
|
property alias contentX: listView.contentX
|
||||||
|
property alias currentItem: listView.currentItem
|
||||||
|
property alias highlightItem: listView.highlightItem
|
||||||
|
property alias headerItem: listView.headerItem
|
||||||
|
property alias footerItem: listView.footerItem
|
||||||
|
property alias section: listView.section
|
||||||
|
property alias highlightFollowsCurrentItem: listView.highlightFollowsCurrentItem
|
||||||
|
property alias highlightMoveDuration: listView.highlightMoveDuration
|
||||||
|
property alias highlightMoveVelocity: listView.highlightMoveVelocity
|
||||||
|
property alias preferredHighlightBegin: listView.preferredHighlightBegin
|
||||||
|
property alias preferredHighlightEnd: listView.preferredHighlightEnd
|
||||||
|
property alias highlightRangeMode: listView.highlightRangeMode
|
||||||
|
property alias snapMode: listView.snapMode
|
||||||
|
property alias keyNavigationWraps: listView.keyNavigationWraps
|
||||||
|
property alias cacheBuffer: listView.cacheBuffer
|
||||||
|
property alias displayMarginBeginning: listView.displayMarginBeginning
|
||||||
|
property alias displayMarginEnd: listView.displayMarginEnd
|
||||||
|
property alias layoutDirection: listView.layoutDirection
|
||||||
|
property alias effectiveLayoutDirection: listView.effectiveLayoutDirection
|
||||||
|
property alias verticalLayoutDirection: listView.verticalLayoutDirection
|
||||||
|
property alias boundsBehavior: listView.boundsBehavior
|
||||||
|
property alias flickableDirection: listView.flickableDirection
|
||||||
|
property alias interactive: listView.interactive
|
||||||
|
property alias moving: listView.moving
|
||||||
|
property alias flicking: listView.flicking
|
||||||
|
property alias dragging: listView.dragging
|
||||||
|
property alias horizontalVelocity: listView.horizontalVelocity
|
||||||
|
property alias verticalVelocity: listView.verticalVelocity
|
||||||
|
|
||||||
|
// Forward ListView methods
|
||||||
|
function positionViewAtIndex(index, mode) {
|
||||||
|
listView.positionViewAtIndex(index, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionViewAtBeginning() {
|
||||||
|
listView.positionViewAtBeginning();
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionViewAtEnd() {
|
||||||
|
listView.positionViewAtEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceLayout() {
|
||||||
|
listView.forceLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelFlick() {
|
||||||
|
listView.cancelFlick();
|
||||||
|
}
|
||||||
|
|
||||||
|
function flick(xVelocity, yVelocity) {
|
||||||
|
listView.flick(xVelocity, yVelocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementCurrentIndex() {
|
||||||
|
listView.incrementCurrentIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrementCurrentIndex() {
|
||||||
|
listView.decrementCurrentIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexAt(x, y) {
|
||||||
|
return listView.indexAt(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemAt(x, y) {
|
||||||
|
return listView.itemAt(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemAtIndex(index) {
|
||||||
|
return listView.itemAtIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set reasonable implicit sizes for Layout usage
|
||||||
|
implicitWidth: 200
|
||||||
|
implicitHeight: 200
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: listView
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
// Enable clipping to keep content within bounds
|
||||||
|
clip: true
|
||||||
|
// Enable flickable for smooth scrolling
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
parent: listView
|
||||||
|
x: listView.mirrored ? 0 : listView.width - width
|
||||||
|
y: 0
|
||||||
|
height: listView.height
|
||||||
|
active: listView.ScrollBar.horizontal.active
|
||||||
|
policy: root.verticalPolicy
|
||||||
|
|
||||||
|
contentItem: Rectangle {
|
||||||
|
implicitWidth: root.handleWidth
|
||||||
|
implicitHeight: 100
|
||||||
|
radius: root.handleRadius
|
||||||
|
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
|
||||||
|
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
implicitWidth: root.handleWidth
|
||||||
|
implicitHeight: 100
|
||||||
|
color: root.trackColor
|
||||||
|
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0
|
||||||
|
radius: root.handleRadius / 2
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.horizontal: ScrollBar {
|
||||||
|
id: horizontalScrollBar
|
||||||
|
|
||||||
|
parent: listView
|
||||||
|
x: 0
|
||||||
|
y: listView.height - height
|
||||||
|
width: listView.width
|
||||||
|
active: listView.ScrollBar.vertical.active
|
||||||
|
policy: root.horizontalPolicy
|
||||||
|
|
||||||
|
contentItem: Rectangle {
|
||||||
|
implicitWidth: 100
|
||||||
|
implicitHeight: root.handleWidth
|
||||||
|
radius: root.handleRadius
|
||||||
|
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
|
||||||
|
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
implicitWidth: 100
|
||||||
|
implicitHeight: root.handleWidth
|
||||||
|
color: root.trackColor
|
||||||
|
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0
|
||||||
|
radius: root.handleRadius / 2
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
459
quickshell/Noctalia/NPanel.qml
Normal file
459
quickshell/Noctalia/NPanel.qml
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
|
||||||
|
property Component panelContent: null
|
||||||
|
property real preferredWidth: 700
|
||||||
|
property real preferredHeight: 900
|
||||||
|
property real preferredWidthRatio
|
||||||
|
property real preferredHeightRatio
|
||||||
|
property color panelBackgroundColor: Color.mSurface
|
||||||
|
property bool draggable: false
|
||||||
|
property var buttonItem: null
|
||||||
|
property string buttonName: ""
|
||||||
|
|
||||||
|
property bool panelAnchorHorizontalCenter: false
|
||||||
|
property bool panelAnchorVerticalCenter: false
|
||||||
|
property bool panelAnchorTop: false
|
||||||
|
property bool panelAnchorBottom: false
|
||||||
|
property bool panelAnchorLeft: false
|
||||||
|
property bool panelAnchorRight: false
|
||||||
|
|
||||||
|
property bool isMasked: false
|
||||||
|
|
||||||
|
// Properties to support positioning relative to the opener (button)
|
||||||
|
property bool useButtonPosition: false
|
||||||
|
property point buttonPosition: Qt.point(0, 0)
|
||||||
|
property int buttonWidth: 0
|
||||||
|
property int buttonHeight: 0
|
||||||
|
|
||||||
|
property bool panelKeyboardFocus: false
|
||||||
|
property bool backgroundClickEnabled: true
|
||||||
|
|
||||||
|
// Animation properties
|
||||||
|
readonly property real originalScale: 0.7
|
||||||
|
readonly property real originalOpacity: 0.0
|
||||||
|
property real scaleValue: originalScale
|
||||||
|
property real opacityValue: originalOpacity
|
||||||
|
property real dimmingOpacity: 0
|
||||||
|
|
||||||
|
signal opened
|
||||||
|
signal closed
|
||||||
|
|
||||||
|
active: false
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
PanelService.registerPanel(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// Functions to control background click behavior
|
||||||
|
function disableBackgroundClick() {
|
||||||
|
backgroundClickEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableBackgroundClick() {
|
||||||
|
// Add a small delay to prevent immediate close after drag release
|
||||||
|
enableBackgroundClickTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: enableBackgroundClickTimer
|
||||||
|
interval: 100
|
||||||
|
repeat: false
|
||||||
|
onTriggered: backgroundClickEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
function toggle(buttonItem, buttonName) {
|
||||||
|
if (!active) {
|
||||||
|
open(buttonItem, buttonName)
|
||||||
|
} else {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
function open(buttonItem, buttonName) {
|
||||||
|
root.buttonItem = buttonItem
|
||||||
|
root.buttonName = buttonName || ""
|
||||||
|
|
||||||
|
setPosition()
|
||||||
|
|
||||||
|
PanelService.willOpenPanel(root)
|
||||||
|
|
||||||
|
backgroundClickEnabled = true
|
||||||
|
active = true
|
||||||
|
root.opened()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
function close() {
|
||||||
|
dimmingOpacity = 0
|
||||||
|
scaleValue = originalScale
|
||||||
|
opacityValue = originalOpacity
|
||||||
|
root.closed()
|
||||||
|
active = false
|
||||||
|
useButtonPosition = false
|
||||||
|
backgroundClickEnabled = true
|
||||||
|
PanelService.closedPanel(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
function setPosition() {
|
||||||
|
// If we have a button name, we are landing here from an IPC call.
|
||||||
|
// IPC calls have no idead on which screen they panel will spawn.
|
||||||
|
// Resolve the button name to a proper button item now that we have a screen.
|
||||||
|
if (buttonName !== "" && root.screen !== null) {
|
||||||
|
buttonItem = BarService.lookupWidget(buttonName, root.screen.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the button position if provided
|
||||||
|
if (buttonItem !== undefined && buttonItem !== null) {
|
||||||
|
useButtonPosition = true
|
||||||
|
var itemPos = buttonItem.mapToItem(null, 0, 0)
|
||||||
|
buttonPosition = Qt.point(itemPos.x, itemPos.y)
|
||||||
|
buttonWidth = buttonItem.width
|
||||||
|
buttonHeight = buttonItem.height
|
||||||
|
} else {
|
||||||
|
useButtonPosition = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
sourceComponent: Component {
|
||||||
|
PanelWindow {
|
||||||
|
id: panelWindow
|
||||||
|
|
||||||
|
readonly property bool isVertical: false
|
||||||
|
readonly property bool barIsVisible: (screen !== null)
|
||||||
|
readonly property real verticalBarWidth: Math.round(Style.barHeight)
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Logger.log("NPanel", "Opened", root.objectName)
|
||||||
|
dimmingOpacity = Style.opacityHeavy
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: panelWindow
|
||||||
|
function onScreenChanged() {
|
||||||
|
root.screen = screen
|
||||||
|
|
||||||
|
// If called from IPC always reposition if screen is updated
|
||||||
|
if (buttonName) {
|
||||||
|
setPosition()
|
||||||
|
}
|
||||||
|
// Logger.log("NPanel", "OnScreenChanged", root.screen.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: true
|
||||||
|
color: Qt.alpha(Color.mShadow, dimmingOpacity)
|
||||||
|
|
||||||
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
|
WlrLayershell.namespace: "noctalia-panel"
|
||||||
|
WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
mask: root.isMasked ? maskRegion : null
|
||||||
|
|
||||||
|
Region {
|
||||||
|
id: maskRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.top: true
|
||||||
|
anchors.left: true
|
||||||
|
anchors.right: true
|
||||||
|
anchors.bottom: true
|
||||||
|
|
||||||
|
// Close any panel with Esc without requiring focus
|
||||||
|
Shortcut {
|
||||||
|
sequences: ["Escape"]
|
||||||
|
enabled: root.active
|
||||||
|
onActivated: root.close()
|
||||||
|
context: Qt.WindowShortcut
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking outside of the rectangle to close
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: root.backgroundClickEnabled
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The actual panel's content
|
||||||
|
Rectangle {
|
||||||
|
id: panelBackground
|
||||||
|
color: panelBackgroundColor
|
||||||
|
radius: Style.radiusL
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Math.max(1, Style.borderS )
|
||||||
|
// Dragging support
|
||||||
|
property bool draggable: root.draggable
|
||||||
|
property bool isDragged: false
|
||||||
|
property real manualX: 0
|
||||||
|
property real manualY: 0
|
||||||
|
width: {
|
||||||
|
var w
|
||||||
|
if (preferredWidthRatio !== undefined) {
|
||||||
|
w = Math.round(Math.max(screen?.width * preferredWidthRatio, preferredWidth) )
|
||||||
|
} else {
|
||||||
|
w = preferredWidth
|
||||||
|
}
|
||||||
|
// Clamp width so it is never bigger than the screen
|
||||||
|
return Math.min(w, screen?.width - Style.marginL * 2)
|
||||||
|
}
|
||||||
|
height: {
|
||||||
|
var h
|
||||||
|
if (preferredHeightRatio !== undefined) {
|
||||||
|
h = Math.round(Math.max(screen?.height * preferredHeightRatio, preferredHeight) )
|
||||||
|
} else {
|
||||||
|
h = preferredHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp width so it is never bigger than the screen
|
||||||
|
return Math.min(h, screen?.height - Style.barHeight - Style.marginL * 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
scale: root.scaleValue
|
||||||
|
opacity: root.isMasked ? 0 : root.opacityValue
|
||||||
|
x: isDragged ? manualX : calculatedX
|
||||||
|
y: isDragged ? manualY : calculatedY
|
||||||
|
|
||||||
|
// ---------------------------------------------
|
||||||
|
// Does not account for corners are they are negligible and helps keep the code clean.
|
||||||
|
// ---------------------------------------------
|
||||||
|
property real marginTop: {
|
||||||
|
if (!barIsVisible) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return (Style.barHeight + Style.marginS)
|
||||||
|
}
|
||||||
|
|
||||||
|
property real marginBottom: {
|
||||||
|
if (!barIsVisible) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Style.marginS
|
||||||
|
}
|
||||||
|
|
||||||
|
property real marginLeft: {
|
||||||
|
if (!barIsVisible) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Style.marginS
|
||||||
|
}
|
||||||
|
|
||||||
|
property real marginRight: {
|
||||||
|
if (!barIsVisible) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Style.marginS
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------
|
||||||
|
property int calculatedX: {
|
||||||
|
// Priority to fixed anchoring
|
||||||
|
if (panelAnchorHorizontalCenter) {
|
||||||
|
// Center horizontally but respect bar margins
|
||||||
|
var centerX = Math.round((panelWindow.width - panelBackground.width) / 2)
|
||||||
|
var minX = marginLeft
|
||||||
|
var maxX = panelWindow.width - panelBackground.width - marginRight
|
||||||
|
return Math.round(Math.max(minX, Math.min(centerX, maxX)))
|
||||||
|
} else if (panelAnchorLeft) {
|
||||||
|
return marginLeft
|
||||||
|
} else if (panelAnchorRight) {
|
||||||
|
return Math.round(panelWindow.width - panelBackground.width - marginRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No fixed anchoring
|
||||||
|
if (isVertical) {
|
||||||
|
// Vertical bar
|
||||||
|
if (barPosition === "right") {
|
||||||
|
// To the left of the right bar
|
||||||
|
return Math.round(panelWindow.width - panelBackground.width - marginRight)
|
||||||
|
} else {
|
||||||
|
// To the right of the left bar
|
||||||
|
return marginLeft
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Horizontal bar
|
||||||
|
if (root.useButtonPosition) {
|
||||||
|
// Position panel relative to button
|
||||||
|
var targetX = buttonPosition.x + (buttonWidth / 2) - (panelBackground.width / 2)
|
||||||
|
// Keep panel within screen bounds
|
||||||
|
var maxX = panelWindow.width - panelBackground.width - marginRight
|
||||||
|
var minX = marginLeft
|
||||||
|
return Math.round(Math.max(minX, Math.min(targetX, maxX)))
|
||||||
|
} else {
|
||||||
|
// Fallback to center horizontally
|
||||||
|
return Math.round((panelWindow.width - panelBackground.width) / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------
|
||||||
|
property int calculatedY: {
|
||||||
|
// Priority to fixed anchoring
|
||||||
|
if (panelAnchorVerticalCenter) {
|
||||||
|
// Center vertically but respect bar margins
|
||||||
|
var centerY = Math.round((panelWindow.height - panelBackground.height) / 2)
|
||||||
|
var minY = marginTop
|
||||||
|
var maxY = panelWindow.height - panelBackground.height - marginBottom
|
||||||
|
return Math.round(Math.max(minY, Math.min(centerY, maxY)))
|
||||||
|
} else if (panelAnchorTop) {
|
||||||
|
return marginTop
|
||||||
|
} else if (panelAnchorBottom) {
|
||||||
|
return Math.round(panelWindow.height - panelBackground.height - marginBottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No fixed anchoring
|
||||||
|
if (isVertical) {
|
||||||
|
// Vertical bar
|
||||||
|
if (useButtonPosition) {
|
||||||
|
// Position panel relative to button
|
||||||
|
var targetY = buttonPosition.y + (buttonHeight / 2) - (panelBackground.height / 2)
|
||||||
|
// Keep panel within screen bounds
|
||||||
|
var maxY = panelWindow.height - panelBackground.height - marginBottom
|
||||||
|
var minY = marginTop
|
||||||
|
return Math.round(Math.max(minY, Math.min(targetY, maxY)))
|
||||||
|
} else {
|
||||||
|
// Fallback to center vertically
|
||||||
|
return Math.round((panelWindow.height - panelBackground.height) / 2)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return marginTop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animate in when component is completed
|
||||||
|
Component.onCompleted: {
|
||||||
|
root.scaleValue = 1.0
|
||||||
|
root.opacityValue = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset drag position when panel closes
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onClosed() {
|
||||||
|
panelBackground.isDragged = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent closing when clicking in the panel bg
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation behaviors
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutExpo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: panelContentLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
sourceComponent: root.panelContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drag move on the whole panel area
|
||||||
|
DragHandler {
|
||||||
|
id: dragHandler
|
||||||
|
target: null
|
||||||
|
enabled: panelBackground.draggable
|
||||||
|
property real dragStartX: 0
|
||||||
|
property real dragStartY: 0
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active) {
|
||||||
|
// Capture current position into manual coordinates BEFORE toggling isDragged
|
||||||
|
panelBackground.manualX = panelBackground.x
|
||||||
|
panelBackground.manualY = panelBackground.y
|
||||||
|
dragStartX = panelBackground.x
|
||||||
|
dragStartY = panelBackground.y
|
||||||
|
panelBackground.isDragged = true
|
||||||
|
if (root.enableBackgroundClick)
|
||||||
|
root.disableBackgroundClick()
|
||||||
|
} else {
|
||||||
|
// Keep isDragged true so we continue using the manual x/y after release
|
||||||
|
if (root.enableBackgroundClick)
|
||||||
|
root.enableBackgroundClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTranslationChanged: {
|
||||||
|
// Proposed new coordinates from fixed drag origin
|
||||||
|
var nx = dragStartX + translation.x
|
||||||
|
var ny = dragStartY + translation.y
|
||||||
|
|
||||||
|
// Calculate gaps so we never overlap the bar on any side
|
||||||
|
var baseGap = Style.marginS
|
||||||
|
var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL : 0
|
||||||
|
var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL : 0
|
||||||
|
|
||||||
|
var insetLeft = baseGap + ((barIsVisible && barPosition === "left") ? (Style.barHeight + floatExtraH) : 0)
|
||||||
|
var insetRight = baseGap + ((barIsVisible && barPosition === "right") ? (Style.barHeight + floatExtraH) : 0)
|
||||||
|
var insetTop = baseGap + ((barIsVisible && barPosition === "top") ? (Style.barHeight + floatExtraV) : 0)
|
||||||
|
var insetBottom = baseGap + ((barIsVisible && barPosition === "bottom") ? (Style.barHeight + floatExtraV) : 0)
|
||||||
|
|
||||||
|
// Clamp within screen bounds accounting for insets
|
||||||
|
var maxX = panelWindow.width - panelBackground.width - insetRight
|
||||||
|
var minX = insetLeft
|
||||||
|
var maxY = panelWindow.height - panelBackground.height - insetBottom
|
||||||
|
var minY = insetTop
|
||||||
|
|
||||||
|
panelBackground.manualX = Math.round(Math.max(minX, Math.min(nx, maxX)))
|
||||||
|
panelBackground.manualY = Math.round(Math.max(minY, Math.min(ny, maxY)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag indicator border
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
color: Color.transparent
|
||||||
|
border.color: Color.mPrimary
|
||||||
|
border.width: Math.max(2, Style.borderL )
|
||||||
|
radius: parent.radius
|
||||||
|
visible: panelBackground.isDragged && dragHandler.active
|
||||||
|
opacity: 0.8
|
||||||
|
z: 3000
|
||||||
|
|
||||||
|
// Subtle glow effect
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
color: Color.transparent
|
||||||
|
border.color: Color.mPrimary
|
||||||
|
border.width: Math.max(1, Style.borderS )
|
||||||
|
radius: parent.radius
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
152
quickshell/Noctalia/NSlider.qml
Normal file
152
quickshell/Noctalia/NSlider.qml
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
Slider {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var cutoutColor: Color.mSurface
|
||||||
|
property bool snapAlways: true
|
||||||
|
property real heightRatio: 0.7
|
||||||
|
readonly property real knobDiameter: Math.round((Style.baseWidgetSize * heightRatio) / 2) * 2
|
||||||
|
readonly property real trackHeight: Math.round((knobDiameter * 0.4) / 2) * 2
|
||||||
|
readonly property real cutoutExtra: Math.round((Style.baseWidgetSize * 0.1) / 2) * 2
|
||||||
|
|
||||||
|
padding: cutoutExtra / 2
|
||||||
|
snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease
|
||||||
|
implicitHeight: Math.max(trackHeight, knobDiameter)
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
x: root.leftPadding
|
||||||
|
y: root.topPadding + root.availableHeight / 2 - height / 2
|
||||||
|
implicitWidth: Style.sliderWidth
|
||||||
|
implicitHeight: trackHeight
|
||||||
|
width: root.availableWidth
|
||||||
|
height: implicitHeight
|
||||||
|
radius: height / 2
|
||||||
|
color: Qt.alpha(Color.mSurface, 0.5)
|
||||||
|
border.color: Qt.alpha(Color.mOutline, 0.5)
|
||||||
|
border.width: Math.max(1, Style.borderS)
|
||||||
|
|
||||||
|
// A container composite shape that puts a semicircle on the end
|
||||||
|
Item {
|
||||||
|
id: activeTrackContainer
|
||||||
|
|
||||||
|
width: root.visualPosition * parent.width
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
// The rounded end cap made from a rounded rectangle
|
||||||
|
Rectangle {
|
||||||
|
width: parent.height
|
||||||
|
height: parent.height
|
||||||
|
radius: width / 2
|
||||||
|
color: Qt.darker(Color.mPrimary, 1.2) //starting color of gradient
|
||||||
|
}
|
||||||
|
|
||||||
|
// The main rectangle
|
||||||
|
Rectangle {
|
||||||
|
x: parent.height / 2
|
||||||
|
width: parent.width - x // Fills the rest of the container
|
||||||
|
height: parent.height
|
||||||
|
radius: 0
|
||||||
|
|
||||||
|
// Animated gradient fill
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Horizontal
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
position: 0
|
||||||
|
color: Qt.darker(Color.mPrimary, 1.2)
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 300
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
position: 0.5
|
||||||
|
color: Color.mPrimary
|
||||||
|
|
||||||
|
SequentialAnimation on position {
|
||||||
|
loops: Animation.Infinite
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
from: 0.3
|
||||||
|
to: 0.7
|
||||||
|
duration: 2000
|
||||||
|
easing.type: Easing.InOutSine
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
from: 0.7
|
||||||
|
to: 0.3
|
||||||
|
duration: 2000
|
||||||
|
easing.type: Easing.InOutSine
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
position: 1
|
||||||
|
color: Qt.lighter(Color.mPrimary, 1.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Circular cutout
|
||||||
|
Rectangle {
|
||||||
|
id: knobCutout
|
||||||
|
|
||||||
|
implicitWidth: knobDiameter + cutoutExtra
|
||||||
|
implicitHeight: knobDiameter + cutoutExtra
|
||||||
|
radius: width / 2
|
||||||
|
color: root.cutoutColor !== undefined ? root.cutoutColor : Color.mSurface
|
||||||
|
x: root.leftPadding + root.visualPosition * (root.availableWidth - root.knobDiameter) - cutoutExtra
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
handle: Item {
|
||||||
|
implicitWidth: knobDiameter
|
||||||
|
implicitHeight: knobDiameter
|
||||||
|
x: root.leftPadding + root.visualPosition * (root.availableWidth - width)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: knob
|
||||||
|
|
||||||
|
implicitWidth: knobDiameter
|
||||||
|
implicitHeight: knobDiameter
|
||||||
|
radius: width / 2
|
||||||
|
color: root.pressed ? Color.mTertiary : Color.mSurface
|
||||||
|
border.color: Color.mPrimary
|
||||||
|
border.width: Math.max(1, Style.borderL)
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
quickshell/Noctalia/NText.qml
Normal file
20
quickshell/Noctalia/NText.qml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string family: Fonts.primary
|
||||||
|
property real pointSize: Style.fontSizeM
|
||||||
|
property real fontScale: 1
|
||||||
|
|
||||||
|
font.family: root.family
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
font.pointSize: root.pointSize * fontScale
|
||||||
|
color: Color.mOnSurface
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Services.Pipewire
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -60,6 +61,7 @@ Singleton {
|
|||||||
|
|
||||||
function onMutedChanged() {
|
function onMutedChanged() {
|
||||||
root._muted = (sink?.audio.muted ?? true)
|
root._muted = (sink?.audio.muted ?? true)
|
||||||
|
Logger.log("AudioService", "OnMuteChanged:", root._muted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ Singleton {
|
|||||||
|
|
||||||
function onMutedChanged() {
|
function onMutedChanged() {
|
||||||
root._inputMuted = (source?.audio.muted ?? true)
|
root._inputMuted = (source?.audio.muted ?? true)
|
||||||
|
Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +96,7 @@ Singleton {
|
|||||||
sink.audio.muted = false
|
sink.audio.muted = false
|
||||||
sink.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
sink.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
||||||
} else {
|
} else {
|
||||||
console.warn("No sink available")
|
Logger.warn("AudioService", "No sink available")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +104,7 @@ Singleton {
|
|||||||
if (sink?.ready && sink?.audio) {
|
if (sink?.ready && sink?.audio) {
|
||||||
sink.audio.muted = muted
|
sink.audio.muted = muted
|
||||||
} else {
|
} else {
|
||||||
console.warn("No sink available")
|
Logger.warn("AudioService", "No sink available")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +114,7 @@ Singleton {
|
|||||||
source.audio.muted = false
|
source.audio.muted = false
|
||||||
source.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
source.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
||||||
} else {
|
} else {
|
||||||
console.warn("No source available")
|
Logger.warn("AudioService", "No source available")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +122,7 @@ Singleton {
|
|||||||
if (source?.ready && source?.audio) {
|
if (source?.ready && source?.audio) {
|
||||||
source.audio.muted = muted
|
source.audio.muted = muted
|
||||||
} else {
|
} else {
|
||||||
console.warn("No source available")
|
Logger.warn("AudioService", "No source available")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pragma Singleton
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -44,6 +45,10 @@ Singleton {
|
|||||||
|
|
||||||
reloadableId: "brightness"
|
reloadableId: "brightness"
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Logger.log("Brightness", "Service started")
|
||||||
|
}
|
||||||
|
|
||||||
onMonitorsChanged: {
|
onMonitorsChanged: {
|
||||||
ddcMonitors = []
|
ddcMonitors = []
|
||||||
ddcProc.running = true
|
ddcProc.running = true
|
||||||
@@ -80,7 +85,7 @@ Singleton {
|
|||||||
var ddcModel = ddcModelMatch ? ddcModelMatch.length > 0 : false
|
var ddcModel = ddcModelMatch ? ddcModelMatch.length > 0 : false
|
||||||
var model = modelMatch ? modelMatch[1] : "Unknown"
|
var model = modelMatch ? modelMatch[1] : "Unknown"
|
||||||
var bus = busMatch ? busMatch[1] : "Unknown"
|
var bus = busMatch ? busMatch[1] : "Unknown"
|
||||||
console.log("Detected DDC Monitor:", model, "on bus", bus, "is DDC:", !ddcModel)
|
Logger.log("Brigthness", "Detected DDC Monitor:", model, "on bus", bus, "is DDC:", !ddcModel)
|
||||||
return {
|
return {
|
||||||
"model": model,
|
"model": model,
|
||||||
"busNum": bus,
|
"busNum": bus,
|
||||||
@@ -188,7 +193,7 @@ Singleton {
|
|||||||
var val = parseInt(dataText)
|
var val = parseInt(dataText)
|
||||||
if (!isNaN(val)) {
|
if (!isNaN(val)) {
|
||||||
monitor.brightness = val / 101
|
monitor.brightness = val / 101
|
||||||
console.log("Apple display brightness:", monitor.brightness)
|
Logger.log("Brightness", "Apple display brightness:", monitor.brightness)
|
||||||
}
|
}
|
||||||
} else if (monitor.isDdc) {
|
} else if (monitor.isDdc) {
|
||||||
var parts = dataText.split(" ")
|
var parts = dataText.split(" ")
|
||||||
@@ -197,7 +202,7 @@ Singleton {
|
|||||||
var max = parseInt(parts[4])
|
var max = parseInt(parts[4])
|
||||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||||
monitor.brightness = current / max
|
monitor.brightness = current / max
|
||||||
console.log("DDC brightness:", monitor.brightness)
|
Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -213,8 +218,8 @@ Singleton {
|
|||||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||||
monitor.maxBrightness = max
|
monitor.maxBrightness = max
|
||||||
monitor.brightness = current / max
|
monitor.brightness = current / max
|
||||||
console.log("Internal brightness:", monitor.brightness)
|
Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
|
||||||
console.log("Using backlight device:", monitor.backlightDevice)
|
Logger.log("Brightness", "Using backlight device:", monitor.backlightDevice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -136,10 +137,10 @@ Singleton {
|
|||||||
if (isInhibited)
|
if (isInhibited)
|
||||||
isInhibited = false;
|
isInhibited = false;
|
||||||
|
|
||||||
console.log("Inhibitor process exited with code:", exitCode, "status:", exitStatus);
|
Logger.log("Caffeine", "Inhibitor process exited with code:", exitCode, "status:", exitStatus);
|
||||||
}
|
}
|
||||||
onStarted: function() {
|
onStarted: function() {
|
||||||
console.log("Inhibitor process started with strategy:", root.strategy);
|
Logger.log("Caffeine", "Inhibitor process started with PID:", inhibitorProcess.processId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
35
quickshell/Services/IPCService.qml
Normal file
35
quickshell/Services/IPCService.qml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Constants
|
||||||
|
|
||||||
|
Item {
|
||||||
|
IpcHandler {
|
||||||
|
function setPrimary(color: color) {
|
||||||
|
SettingsService.primaryColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "colors"
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function toggleCalendar() {
|
||||||
|
calendarPanel.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleControlCenter() {
|
||||||
|
controlCenterPanel.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "panels"
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function toggleBarLyrics() {
|
||||||
|
SettingsService.showLyricsBar = !SettingsService.showLyricsBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "lyrics"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import qs.Utils
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -9,28 +10,93 @@ Singleton {
|
|||||||
property real fetchInterval: 30 // in s
|
property real fetchInterval: 30 // in s
|
||||||
property real fetchTimeout: 10 // in s
|
property real fetchTimeout: 10 // in s
|
||||||
property string ipURL: "https://api.uyanide.com/ip"
|
property string ipURL: "https://api.uyanide.com/ip"
|
||||||
property string geoURL: "curl https://api.ipinfo.io/lite/"
|
property string geoURL: "https://api.ipinfo.io/lite/"
|
||||||
property string geoURLToken: ""
|
property string geoURLToken: ""
|
||||||
|
|
||||||
function fetchIP() {
|
function fetchIP() {
|
||||||
if (fetchIPProcess.running) {
|
const xhr = new XMLHttpRequest();
|
||||||
console.warn("Fetch IP process is still running, skipping fetchIP");
|
xhr.timeout = fetchTimeout * 1000;
|
||||||
return ;
|
xhr.onreadystatechange = function() {
|
||||||
}
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
fetchIPProcess.running = true;
|
if (xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(xhr.responseText);
|
||||||
|
if (response && response.ip) {
|
||||||
|
let newIP = response.ip;
|
||||||
|
Logger.log("IpService", "Fetched IP: " + newIP);
|
||||||
|
if (newIP !== ip) {
|
||||||
|
ip = newIP;
|
||||||
|
fetchGeoInfo(); // Fetch geo info only if IP has changed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ip = "N/A";
|
||||||
|
countryCode = "N/A";
|
||||||
|
Logger.error("IpService", "IP response does not contain 'ip' field");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ip = "N/A";
|
||||||
|
countryCode = "N/A";
|
||||||
|
Logger.error("IpService", "Failed to parse IP response: " + e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ip = "N/A";
|
||||||
|
countryCode = "N/A";
|
||||||
|
Logger.error("IpService", "Failed to fetch IP, status: " + xhr.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.ontimeout = function() {
|
||||||
|
ip = "N/A";
|
||||||
|
countryCode = "N/A";
|
||||||
|
Logger.error("IpService", "Fetch IP request timed out");
|
||||||
|
};
|
||||||
|
xhr.open("GET", ipURL);
|
||||||
|
xhr.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchGeoInfo() {
|
function fetchGeoInfo() {
|
||||||
if (fetchGeoProcess.running) {
|
|
||||||
console.warn("Fetch geo process is still running, skipping fetchGeoInfo");
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (!ip || ip === "N/A") {
|
if (!ip || ip === "N/A") {
|
||||||
countryCode = "N/A";
|
countryCode = "N/A";
|
||||||
return ;
|
return ;
|
||||||
}
|
}
|
||||||
fetchGeoProcess.command = ["sh", "-c", `curl -L -m ${fetchTimeout.toString()} ${geoURL}${ip}${geoURLToken ? "?token=" + geoURLToken : ""}`];
|
const xhr = new XMLHttpRequest();
|
||||||
fetchGeoProcess.running = true;
|
xhr.timeout = fetchTimeout * 1000;
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(xhr.responseText);
|
||||||
|
if (response && response.country) {
|
||||||
|
let newCountryCode = response.country_code;
|
||||||
|
Logger.log("IpService", "Fetched country code: " + newCountryCode);
|
||||||
|
if (newCountryCode !== countryCode) {
|
||||||
|
countryCode = newCountryCode;
|
||||||
|
SendNotification.show("New IP", `IP: ${ip}\nCountry: ${newCountryCode}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
countryCode = "N/A";
|
||||||
|
Logger.error("IpService", "Geo response does not contain 'country' field");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
countryCode = "N/A";
|
||||||
|
Logger.error("IpService", "Failed to parse geo response: " + e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
countryCode = "N/A";
|
||||||
|
Logger.error("IpService", "Failed to fetch geo info, status: " + xhr.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.ontimeout = function() {
|
||||||
|
countryCode = "N/A";
|
||||||
|
Logger.error("IpService", "Fetch geo info request timed out");
|
||||||
|
};
|
||||||
|
let url = geoURL + ip;
|
||||||
|
if (geoURLToken)
|
||||||
|
url += "?token=" + geoURLToken;
|
||||||
|
|
||||||
|
xhr.open("GET", url);
|
||||||
|
xhr.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
@@ -46,11 +112,11 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: tokenFile
|
id: tokenFile
|
||||||
|
|
||||||
path: Qt.resolvedUrl("../Assets/Ip/token.txt")
|
path: Qt.resolvedUrl("../Assets/Config/GeoInfoToken.txt")
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
geoURLToken = tokenFile.text();
|
geoURLToken = tokenFile.text();
|
||||||
if (!geoURLToken)
|
if (!geoURLToken)
|
||||||
console.warn("No token found for geoIP service, assuming none is required");
|
Logger.warn("IpService", "No token found for geoIP service, assuming none is required");
|
||||||
|
|
||||||
fetchIP();
|
fetchIP();
|
||||||
fetchTimer.start();
|
fetchTimer.start();
|
||||||
@@ -68,64 +134,4 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: fetchIPProcess
|
|
||||||
|
|
||||||
command: ["sh", "-c", `curl -L -m ${fetchTimeout.toString()} ${ipURL}`]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
splitMarker: ""
|
|
||||||
onRead: (data) => {
|
|
||||||
let newIP = "";
|
|
||||||
try {
|
|
||||||
const response = JSON.parse(data);
|
|
||||||
if (response && response.ip) {
|
|
||||||
newIP = response.ip;
|
|
||||||
console.log("Fetched IP: " + newIP);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to parse IP response: " + e);
|
|
||||||
}
|
|
||||||
if (newIP && newIP !== ip) {
|
|
||||||
ip = newIP;
|
|
||||||
fetchGeoInfo();
|
|
||||||
} else if (!newIP) {
|
|
||||||
ip = "N/A";
|
|
||||||
countryCode = "N/A";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: fetchGeoProcess
|
|
||||||
|
|
||||||
command: []
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
splitMarker: ""
|
|
||||||
onRead: (data) => {
|
|
||||||
let newCountryCode = "";
|
|
||||||
try {
|
|
||||||
const response = JSON.parse(data);
|
|
||||||
if (response && response.country) {
|
|
||||||
newCountryCode = response.country_code;
|
|
||||||
console.log("Fetched country code: " + newCountryCode);
|
|
||||||
SendNotification.show("New IP", `IP: ${ip}\nCountry: ${newCountryCode}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to parse geo response: " + e);
|
|
||||||
}
|
|
||||||
if (newCountryCode && newCountryCode !== countryCode)
|
|
||||||
countryCode = newCountryCode;
|
|
||||||
else if (!newCountryCode)
|
|
||||||
countryCode = "N/A";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
323
quickshell/Services/LocationService.qml
Normal file
323
quickshell/Services/LocationService.qml
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Utils
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
// Weather logic and caching with stable UI properties
|
||||||
|
Singleton {
|
||||||
|
//console.log(JSON.stringify(weatherData))
|
||||||
|
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string locationName: "Munich"
|
||||||
|
property string locationFile: Qt.resolvedUrl("../Assets/Config/Location.json")
|
||||||
|
property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds
|
||||||
|
property bool isFetchingWeather: false
|
||||||
|
readonly property alias data: adapter // Used to access via LocationService.data.xxx from outside, best to use "adapter" inside the service.
|
||||||
|
// Stable UI properties - only updated when location is fully resolved
|
||||||
|
property bool coordinatesReady: false
|
||||||
|
property string stableLatitude: ""
|
||||||
|
property string stableLongitude: ""
|
||||||
|
property string stableName: ""
|
||||||
|
// Helper property for UI components (outside JsonAdapter to avoid binding loops)
|
||||||
|
readonly property string displayCoordinates: {
|
||||||
|
if (!root.coordinatesReady || root.stableLatitude === "" || root.stableLongitude === "")
|
||||||
|
return "";
|
||||||
|
|
||||||
|
const lat = parseFloat(root.stableLatitude).toFixed(4);
|
||||||
|
const lon = parseFloat(root.stableLongitude).toFixed(4);
|
||||||
|
return `${lat}, ${lon}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
function init() {
|
||||||
|
// does nothing but ensure the singleton is created
|
||||||
|
// do not remove
|
||||||
|
Logger.log("Location", "Service started");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
function resetWeather() {
|
||||||
|
Logger.log("Location", "Resetting weather data");
|
||||||
|
// Mark as changing to prevent UI updates
|
||||||
|
root.coordinatesReady = false;
|
||||||
|
// Reset stable properties
|
||||||
|
root.stableLatitude = "";
|
||||||
|
root.stableLongitude = "";
|
||||||
|
root.stableName = "";
|
||||||
|
// Reset core data
|
||||||
|
adapter.latitude = "";
|
||||||
|
adapter.longitude = "";
|
||||||
|
adapter.name = "";
|
||||||
|
adapter.weatherLastFetch = 0;
|
||||||
|
adapter.weather = null;
|
||||||
|
// Try to fetch immediately
|
||||||
|
updateWeather();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
function updateWeather() {
|
||||||
|
if (isFetchingWeather) {
|
||||||
|
Logger.warn("Location", "Weather is still fetching");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
if ((adapter.weatherLastFetch === "") || (adapter.weather === null) || (adapter.latitude === "") || (adapter.longitude === "") || (adapter.name !== root.locationName) || (Time.timestamp >= adapter.weatherLastFetch + weatherUpdateFrequency))
|
||||||
|
getFreshWeather();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
function getFreshWeather() {
|
||||||
|
isFetchingWeather = true;
|
||||||
|
// Check if location name has changed
|
||||||
|
const locationChanged = data.name !== root.locationName;
|
||||||
|
if (locationChanged) {
|
||||||
|
root.coordinatesReady = false;
|
||||||
|
Logger.log("Location", "Location changed from", adapter.name, "to", root.locationName);
|
||||||
|
}
|
||||||
|
if ((adapter.latitude === "") || (adapter.longitude === "") || locationChanged)
|
||||||
|
_geocodeLocation(root.locationName, function(latitude, longitude, name, country) {
|
||||||
|
Logger.log("Location", "Geocoded", root.locationName, "to:", latitude, "/", longitude);
|
||||||
|
// Save location name
|
||||||
|
adapter.name = root.locationName;
|
||||||
|
// Save GPS coordinates
|
||||||
|
adapter.latitude = latitude.toString();
|
||||||
|
adapter.longitude = longitude.toString();
|
||||||
|
root.stableName = `${name}, ${country}`;
|
||||||
|
_fetchWeather(latitude, longitude, errorCallback);
|
||||||
|
}, errorCallback);
|
||||||
|
else
|
||||||
|
_fetchWeather(adapter.latitude, adapter.longitude, errorCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
function _geocodeLocation(locationName, callback, errorCallback) {
|
||||||
|
Logger.log("Location", "Geocoding location name");
|
||||||
|
var geoUrl = "https://assets.noctalia.dev/geocode.php?city=" + encodeURIComponent(locationName) + "&language=en&format=json";
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
var geoData = JSON.parse(xhr.responseText);
|
||||||
|
if (geoData.lat != null)
|
||||||
|
callback(geoData.lat, geoData.lng, geoData.name, geoData.country);
|
||||||
|
else
|
||||||
|
errorCallback("Location", "could not resolve location name");
|
||||||
|
} catch (e) {
|
||||||
|
errorCallback("Location", "Failed to parse geocoding data: " + e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorCallback("Location", "Geocoding error: " + xhr.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.open("GET", geoUrl);
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
function _fetchWeather(latitude, longitude, errorCallback) {
|
||||||
|
Logger.log("Location", "Fetching weather from api.open-meteo.com");
|
||||||
|
var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto";
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
var weatherData = JSON.parse(xhr.responseText);
|
||||||
|
// Save core data
|
||||||
|
data.weather = weatherData;
|
||||||
|
data.weatherLastFetch = Time.timestamp;
|
||||||
|
// Update stable display values only when complete and successful
|
||||||
|
root.stableLatitude = data.latitude = weatherData.latitude.toString();
|
||||||
|
root.stableLongitude = data.longitude = weatherData.longitude.toString();
|
||||||
|
root.coordinatesReady = true;
|
||||||
|
isFetchingWeather = false;
|
||||||
|
Logger.log("Location", "Cached weather to disk - stable coordinates updated");
|
||||||
|
} catch (e) {
|
||||||
|
errorCallback("Location", "Failed to parse weather data");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorCallback("Location", "Weather fetch error: " + xhr.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.open("GET", url);
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
function errorCallback(module, message) {
|
||||||
|
Logger.error(module, message);
|
||||||
|
isFetchingWeather = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
function weatherSymbolFromCode(code) {
|
||||||
|
if (code === 0)
|
||||||
|
return "weather-sun";
|
||||||
|
|
||||||
|
if (code === 1 || code === 2)
|
||||||
|
return "weather-cloud-sun";
|
||||||
|
|
||||||
|
if (code === 3)
|
||||||
|
return "weather-cloud";
|
||||||
|
|
||||||
|
if (code >= 45 && code <= 48)
|
||||||
|
return "weather-cloud-haze";
|
||||||
|
|
||||||
|
if (code >= 51 && code <= 67)
|
||||||
|
return "weather-cloud-rain";
|
||||||
|
|
||||||
|
if (code >= 71 && code <= 77)
|
||||||
|
return "weather-cloud-snow";
|
||||||
|
|
||||||
|
if (code >= 71 && code <= 77)
|
||||||
|
return "weather-cloud-snow";
|
||||||
|
|
||||||
|
if (code >= 85 && code <= 86)
|
||||||
|
return "weather-cloud-snow";
|
||||||
|
|
||||||
|
if (code >= 95 && code <= 99)
|
||||||
|
return "weather-cloud-lightning";
|
||||||
|
|
||||||
|
return "weather-cloud";
|
||||||
|
}
|
||||||
|
|
||||||
|
function weatherColorFromCode(code) {
|
||||||
|
// Clear sky - bright yellow
|
||||||
|
if (code === 0)
|
||||||
|
return Colors.yellow;
|
||||||
|
|
||||||
|
// Mainly clear/Partly cloudy - soft peach/rosewater tones
|
||||||
|
if (code === 1 || code === 2)
|
||||||
|
return Colors.peach;
|
||||||
|
|
||||||
|
// Overcast - neutral sky blue
|
||||||
|
if (code === 3)
|
||||||
|
return Colors.sky;
|
||||||
|
|
||||||
|
// Fog - soft lavender/muted tone
|
||||||
|
if (code >= 45 && code <= 48)
|
||||||
|
return Colors.lavender;
|
||||||
|
|
||||||
|
// Drizzle - light blue/sapphire
|
||||||
|
if (code >= 51 && code <= 67)
|
||||||
|
return Colors.sapphire;
|
||||||
|
|
||||||
|
// Snow - cool teal
|
||||||
|
if (code >= 71 && code <= 77)
|
||||||
|
return Colors.teal;
|
||||||
|
|
||||||
|
// Rain showers - deeper blue
|
||||||
|
if (code >= 80 && code <= 82)
|
||||||
|
return Colors.blue;
|
||||||
|
|
||||||
|
// Snow showers - teal
|
||||||
|
if (code >= 85 && code <= 86)
|
||||||
|
return Colors.teal;
|
||||||
|
|
||||||
|
// Thunderstorm - dramatic mauve/pink
|
||||||
|
if (code >= 95 && code <= 99)
|
||||||
|
return Colors.mauve;
|
||||||
|
|
||||||
|
// Default - sky blue
|
||||||
|
return Colors.sky;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
function weatherDescriptionFromCode(code) {
|
||||||
|
if (code === 0)
|
||||||
|
return "Clear sky";
|
||||||
|
|
||||||
|
if (code === 1)
|
||||||
|
return "Mainly clear";
|
||||||
|
|
||||||
|
if (code === 2)
|
||||||
|
return "Partly cloudy";
|
||||||
|
|
||||||
|
if (code === 3)
|
||||||
|
return "Overcast";
|
||||||
|
|
||||||
|
if (code === 45 || code === 48)
|
||||||
|
return "Fog";
|
||||||
|
|
||||||
|
if (code >= 51 && code <= 67)
|
||||||
|
return "Drizzle";
|
||||||
|
|
||||||
|
if (code >= 71 && code <= 77)
|
||||||
|
return "Snow";
|
||||||
|
|
||||||
|
if (code >= 80 && code <= 82)
|
||||||
|
return "Rain showers";
|
||||||
|
|
||||||
|
if (code >= 95 && code <= 99)
|
||||||
|
return "Thunderstorm";
|
||||||
|
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
function celsiusToFahrenheit(celsius) {
|
||||||
|
return 32 + celsius * 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: locationFileView
|
||||||
|
|
||||||
|
path: locationFile
|
||||||
|
printErrors: false
|
||||||
|
onAdapterUpdated: saveTimer.start()
|
||||||
|
onLoaded: {
|
||||||
|
Logger.log("Location", "Loaded cached data");
|
||||||
|
// Initialize stable properties on load
|
||||||
|
if (adapter.latitude !== "" && adapter.longitude !== "" && adapter.weatherLastFetch > 0) {
|
||||||
|
root.stableLatitude = adapter.latitude;
|
||||||
|
root.stableLongitude = adapter.longitude;
|
||||||
|
root.stableName = adapter.name;
|
||||||
|
root.coordinatesReady = true;
|
||||||
|
Logger.log("Location", "Coordinates ready");
|
||||||
|
}
|
||||||
|
updateWeather();
|
||||||
|
}
|
||||||
|
onLoadFailed: function(error) {
|
||||||
|
updateWeather();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonAdapter {
|
||||||
|
id: adapter
|
||||||
|
|
||||||
|
// Core data properties
|
||||||
|
property string latitude: ""
|
||||||
|
property string longitude: ""
|
||||||
|
property string name: ""
|
||||||
|
property int weatherLastFetch: 0
|
||||||
|
property var weather: null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every 20s check if we need to fetch new weather
|
||||||
|
Timer {
|
||||||
|
id: updateTimer
|
||||||
|
|
||||||
|
interval: 20 * 1000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
updateWeather();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: saveTimer
|
||||||
|
|
||||||
|
running: false
|
||||||
|
interval: 1000
|
||||||
|
onTriggered: locationFileView.writeAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
144
quickshell/Services/LyricsService.qml
Normal file
144
quickshell/Services/LyricsService.qml
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
property int linesCount: 3
|
||||||
|
property int linesAhead: linesCount / 2
|
||||||
|
property int currentIndex: linesCount - linesAhead - 1
|
||||||
|
property string offsetFile: Qt.resolvedUrl("../Assets/Config/LyricsOffset.txt")
|
||||||
|
property int offset: 0 // in ms
|
||||||
|
property int offsetStep: 500 // in ms
|
||||||
|
property int referenceCount: 0
|
||||||
|
// with linesCount=3 and linesAhead=1, lyrics will be like:
|
||||||
|
// line 1
|
||||||
|
// line 2 <- current line
|
||||||
|
// line 3
|
||||||
|
property var lyrics: Array(linesCount).fill(" ")
|
||||||
|
|
||||||
|
function startSyncing() {
|
||||||
|
referenceCount++;
|
||||||
|
Logger.log("LyricsService", "Reference count:", referenceCount);
|
||||||
|
if (referenceCount === 1) {
|
||||||
|
Logger.log("LyricsService", "Starting lyrics syncing");
|
||||||
|
// fill lyrics with empty lines
|
||||||
|
lyrics = Array(linesCount).fill(" ");
|
||||||
|
listenProcess.exec(["sh", "-c", `sl-wrap listen -l ${linesCount} -a ${linesAhead} -f ${offsetFile.slice(7)}`]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSyncing() {
|
||||||
|
referenceCount--;
|
||||||
|
Logger.log("LyricsService", "Reference count:", referenceCount);
|
||||||
|
if (referenceCount <= 0) {
|
||||||
|
Logger.log("LyricsService", "Stopping lyrics syncing");
|
||||||
|
// Execute again to stop
|
||||||
|
// kinda ugly but works, but meanwhile:
|
||||||
|
// listenProcess.signal(9)
|
||||||
|
// listenProcess.signal(15)
|
||||||
|
// listenProcess.running = false
|
||||||
|
// counts on exec() to terminate previous exec()
|
||||||
|
// all don't work
|
||||||
|
listenProcess.exec(["sh", "-c", `sl-wrap trackid`]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeOffset() {
|
||||||
|
offsetFileView.setText(String(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
function increaseOffset() {
|
||||||
|
offset += offsetStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decreaseOffset() {
|
||||||
|
offset -= offsetStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetOffset() {
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCache() {
|
||||||
|
action.command = ["sh", "-c", "spotify-lyrics clear $(spotify-lyrics trackid)"];
|
||||||
|
action.startDetached();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLyricsText() {
|
||||||
|
action.command = ["sh", "-c", "ghostty -e sh -c 'spotify-lyrics fetch | less'"];
|
||||||
|
action.startDetached();
|
||||||
|
}
|
||||||
|
|
||||||
|
onOffsetChanged: {
|
||||||
|
if (SettingsService.showLyricsBar)
|
||||||
|
SendNotification.show("Lyrics Offset Changed", `Current offset: ${offset} ms`, 1000);
|
||||||
|
|
||||||
|
writeOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: listenProcess
|
||||||
|
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: ""
|
||||||
|
onRead: (data) => {
|
||||||
|
lyrics = data.split("\n").slice(0, linesCount);
|
||||||
|
if (lyrics.length < linesCount) {
|
||||||
|
// fill with empty lines if not enough
|
||||||
|
for (let i = lyrics.length; i < linesCount; i++) {
|
||||||
|
lyrics[i] = " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: action
|
||||||
|
|
||||||
|
running: false
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: offsetFileView
|
||||||
|
|
||||||
|
path: offsetFile
|
||||||
|
watchChanges: false
|
||||||
|
onLoaded: {
|
||||||
|
try {
|
||||||
|
const fileContents = text();
|
||||||
|
if (fileContents.length > 0) {
|
||||||
|
const val = parseInt(fileContents);
|
||||||
|
if (!isNaN(val)) {
|
||||||
|
offset = val;
|
||||||
|
Logger.log("LyricsService", "Loaded offset:", offset);
|
||||||
|
} else {
|
||||||
|
offset = 0;
|
||||||
|
writeOffset();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
offset = 0;
|
||||||
|
writeOffset();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.log("LyricsService", "Error reading offset file:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
Logger.log("LyricsService", "Error loading offset file:", errorString);
|
||||||
|
}
|
||||||
|
onSaveFailed: {
|
||||||
|
Logger.log("LyricsService", "Error saving offset file:", errorString);
|
||||||
|
}
|
||||||
|
onSaved: {
|
||||||
|
Logger.log("LyricsService", "Offset file saved.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
import qs.Modules.Misc
|
import qs.Modules.Misc
|
||||||
|
import qs.Utils
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -11,6 +12,7 @@ Singleton {
|
|||||||
property var currentPlayer: null
|
property var currentPlayer: null
|
||||||
property real currentPosition: 0
|
property real currentPosition: 0
|
||||||
property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false
|
property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false
|
||||||
|
property int selectedPlayerIndex: -1
|
||||||
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
|
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
|
||||||
property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
|
property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
|
||||||
property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
|
property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
|
||||||
@@ -59,11 +61,25 @@ Singleton {
|
|||||||
|
|
||||||
// Updates currentPlayer and currentPosition
|
// Updates currentPlayer and currentPosition
|
||||||
function updateCurrentPlayer() {
|
function updateCurrentPlayer() {
|
||||||
|
// Use selected player if index is valid
|
||||||
|
if (selectedPlayerIndex >= 0) {
|
||||||
|
let availablePlayers = getAvailablePlayers();
|
||||||
|
if (selectedPlayerIndex < availablePlayers.length) {
|
||||||
|
currentPlayer = availablePlayers[selectedPlayerIndex];
|
||||||
|
currentPosition = currentPlayer.position;
|
||||||
|
Logger.log("MusicManager", "Current player set by index:", currentPlayer ? currentPlayer.identity : "None");
|
||||||
|
return ;
|
||||||
|
} else {
|
||||||
|
selectedPlayerIndex = -1; // Reset if index is out of range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise, find active player
|
||||||
let newPlayer = findActivePlayer();
|
let newPlayer = findActivePlayer();
|
||||||
if (newPlayer !== currentPlayer) {
|
if (newPlayer !== currentPlayer) {
|
||||||
currentPlayer = newPlayer;
|
currentPlayer = newPlayer;
|
||||||
currentPosition = currentPlayer ? currentPlayer.position : 0;
|
currentPosition = currentPlayer ? currentPlayer.position : 0;
|
||||||
}
|
}
|
||||||
|
Logger.log("MusicManager", "Current player updated:", currentPlayer ? currentPlayer.identity : "None");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Player control functions
|
// Player control functions
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import qs.Utils
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ Singleton {
|
|||||||
});
|
});
|
||||||
root.workspaces = workspacesList;
|
root.workspaces = workspacesList;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to parse workspaces:", e, line);
|
Logger.error("Niri", "Failed to parse workspaces:", e, line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +103,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
root.windows = windowsMap;
|
root.windows = windowsMap;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing windows event:", e);
|
Logger.error("Niri", "Error parsing windows event:", e);
|
||||||
}
|
}
|
||||||
} else if (event.WorkspaceActivated) {
|
} else if (event.WorkspaceActivated) {
|
||||||
workspaceProcess.running = true;
|
workspaceProcess.running = true;
|
||||||
@@ -120,13 +121,13 @@ Singleton {
|
|||||||
root.focusedWindowId = -1;
|
root.focusedWindowId = -1;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing window focus event:", e);
|
Logger.error("Niri", "Error parsing window focus event:", e);
|
||||||
}
|
}
|
||||||
} else if (event.OverviewOpenedOrClosed) {
|
} else if (event.OverviewOpenedOrClosed) {
|
||||||
try {
|
try {
|
||||||
root.inOverview = event.OverviewOpenedOrClosed.is_open === true;
|
root.inOverview = event.OverviewOpenedOrClosed.is_open === true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing overview state:", e);
|
Logger.error("Niri", "Error parsing overview state:", e);
|
||||||
}
|
}
|
||||||
} else if (event.WindowOpenedOrChanged) {
|
} else if (event.WindowOpenedOrChanged) {
|
||||||
try {
|
try {
|
||||||
@@ -161,7 +162,7 @@ Singleton {
|
|||||||
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing window opened/changed event:", e);
|
Logger.error("Niri", "Error parsing window opened/changed event:", e);
|
||||||
}
|
}
|
||||||
} else if (event.windowClosed) {
|
} else if (event.windowClosed) {
|
||||||
try {
|
try {
|
||||||
@@ -170,11 +171,11 @@ Singleton {
|
|||||||
delete root.windows[closedId];
|
delete root.windows[closedId];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing window closed event:", e);
|
Logger.error("Niri", "Error parsing window closed event:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing event stream:", e, data);
|
Logger.error("Niri", "Error parsing event stream:", e, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
88
quickshell/Services/PowerProfileService.qml
Normal file
88
quickshell/Services/PowerProfileService.qml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var powerProfiles: PowerProfiles
|
||||||
|
readonly property bool available: powerProfiles && powerProfiles.hasPerformanceProfile
|
||||||
|
property int profile: powerProfiles ? powerProfiles.profile : PowerProfile.Balanced
|
||||||
|
|
||||||
|
function getName(p) {
|
||||||
|
if (!available)
|
||||||
|
return "Unknown";
|
||||||
|
|
||||||
|
const prof = (p !== undefined) ? p : profile;
|
||||||
|
switch (prof) {
|
||||||
|
case PowerProfile.Performance:
|
||||||
|
return "Performance";
|
||||||
|
case PowerProfile.Balanced:
|
||||||
|
return "Balanced";
|
||||||
|
case PowerProfile.PowerSaver:
|
||||||
|
return "Power saver";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIcon(p) {
|
||||||
|
if (!available)
|
||||||
|
return "balanced";
|
||||||
|
|
||||||
|
const prof = (p !== undefined) ? p : profile;
|
||||||
|
switch (prof) {
|
||||||
|
case PowerProfile.Performance:
|
||||||
|
return "performance";
|
||||||
|
case PowerProfile.Balanced:
|
||||||
|
return "balanced";
|
||||||
|
case PowerProfile.PowerSaver:
|
||||||
|
return "powersaver";
|
||||||
|
default:
|
||||||
|
return "balanced";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProfile(p) {
|
||||||
|
if (!available)
|
||||||
|
return ;
|
||||||
|
|
||||||
|
try {
|
||||||
|
powerProfiles.profile = p;
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error("PowerProfileService", "Failed to set profile:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cycleProfile() {
|
||||||
|
if (!available)
|
||||||
|
return ;
|
||||||
|
|
||||||
|
const current = powerProfiles.profile;
|
||||||
|
if (current === PowerProfile.Performance)
|
||||||
|
setProfile(PowerProfile.PowerSaver);
|
||||||
|
else if (current === PowerProfile.Balanced)
|
||||||
|
setProfile(PowerProfile.Performance);
|
||||||
|
else if (current === PowerProfile.PowerSaver)
|
||||||
|
setProfile(PowerProfile.Balanced);
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onProfileChanged() {
|
||||||
|
root.profile = powerProfiles.profile;
|
||||||
|
// Only show toast if we have a valid profile name (not "Unknown")
|
||||||
|
const profileName = root.getName();
|
||||||
|
if (profileName !== "Unknown")
|
||||||
|
ToastService.showNotice(I18n.tr("toast.power-profile.changed"), I18n.tr("toast.power-profile.profile-name", {
|
||||||
|
"profile": profileName
|
||||||
|
}));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
target: powerProfiles
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ pragma Singleton
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
function show(title, message, icon = "", urgency = "normal", timeout = 5000) {
|
function show(title, message, timeout = 5000, icon = "", urgency = "normal") {
|
||||||
if (icon)
|
if (icon)
|
||||||
action.command = ["notify-send", "-u", urgency, "-i", icon, "-t", timeout.toString(), title, message];
|
action.command = ["notify-send", "-u", urgency, "-i", icon, "-t", timeout.toString(), title, message];
|
||||||
else
|
else
|
||||||
|
|||||||
35
quickshell/Services/SettingsService.qml
Normal file
35
quickshell/Services/SettingsService.qml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Services
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
property alias primaryColor: adapter.primaryColor
|
||||||
|
property alias showLyricsBar: adapter.showLyricsBar
|
||||||
|
property string settingsFilePath: Qt.resolvedUrl("../Assets/Config/Settings.json")
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: settingsFile
|
||||||
|
|
||||||
|
path: settingsFilePath
|
||||||
|
watchChanges: true
|
||||||
|
onFileChanged: reload()
|
||||||
|
|
||||||
|
JsonAdapter {
|
||||||
|
id: adapter
|
||||||
|
|
||||||
|
property string primaryColor: "#89b4fa"
|
||||||
|
property bool showLyricsBar: false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: adapter
|
||||||
|
onPrimaryColorChanged: settingsFile.writeAdapter()
|
||||||
|
onShowLyricsBarChanged: settingsFile.writeAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import Qt.labs.folderlistmodel
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import qs.Utils
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -222,7 +223,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
root.cpuTemp = Math.round(sum / root.intelTempValues.length);
|
root.cpuTemp = Math.round(sum / root.intelTempValues.length);
|
||||||
} else {
|
} else {
|
||||||
console.warn("No temperature sensors found for coretemp");
|
Logger.warn("SystemStatService", "No temperature sensors found for coretemp");
|
||||||
root.cpuTemp = 0;
|
root.cpuTemp = 0;
|
||||||
}
|
}
|
||||||
return ;
|
return ;
|
||||||
@@ -328,7 +329,7 @@ Singleton {
|
|||||||
function checkNext() {
|
function checkNext() {
|
||||||
if (currentIndex >= 16) {
|
if (currentIndex >= 16) {
|
||||||
// Check up to hwmon10
|
// Check up to hwmon10
|
||||||
console.warn("No supported temperature sensor found");
|
Logger.warn("SystemStatService", "No supported temperature sensor found");
|
||||||
return ;
|
return ;
|
||||||
}
|
}
|
||||||
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`;
|
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`;
|
||||||
@@ -341,7 +342,7 @@ Singleton {
|
|||||||
if (root.supportedTempCpuSensorNames.includes(name)) {
|
if (root.supportedTempCpuSensorNames.includes(name)) {
|
||||||
root.cpuTempSensorName = name;
|
root.cpuTempSensorName = name;
|
||||||
root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`;
|
root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`;
|
||||||
console.log(`Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`);
|
Logger.log("SystemStatService", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`);
|
||||||
} else {
|
} else {
|
||||||
currentIndex++;
|
currentIndex++;
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
@@ -370,7 +371,6 @@ Singleton {
|
|||||||
if (root.cpuTempSensorName === "coretemp") {
|
if (root.cpuTempSensorName === "coretemp") {
|
||||||
// For Intel, collect all temperature values
|
// For Intel, collect all temperature values
|
||||||
const temp = parseInt(data) / 1000;
|
const temp = parseInt(data) / 1000;
|
||||||
//console.log(temp, cpuTempReader.path)
|
|
||||||
root.intelTempValues.push(temp);
|
root.intelTempValues.push(temp);
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
// Qt.callLater is mandatory
|
// Qt.callLater is mandatory
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -40,7 +41,7 @@ Singleton {
|
|||||||
try {
|
try {
|
||||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]);
|
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error switching Niri workspace:", e);
|
Logger.error("WorkspaceManager", "Error switching Niri workspace:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
quickshell/Shaders/qsb/circled_image.frag.qsb
Normal file
BIN
quickshell/Shaders/qsb/circled_image.frag.qsb
Normal file
Binary file not shown.
BIN
quickshell/Shaders/qsb/rounded_image.frag.qsb
Normal file
BIN
quickshell/Shaders/qsb/rounded_image.frag.qsb
Normal file
Binary file not shown.
@@ -23,7 +23,9 @@ Scope {
|
|||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"method": "raw",
|
"method": "raw",
|
||||||
"bit_format": 8,
|
"data_format": "ascii",
|
||||||
|
"ascii_max_range": 100,
|
||||||
|
"bit_format": "8bit",
|
||||||
"channels": channels,
|
"channels": channels,
|
||||||
"mono_option": monoOption
|
"mono_option": monoOption
|
||||||
}
|
}
|
||||||
@@ -33,14 +35,11 @@ Scope {
|
|||||||
Process {
|
Process {
|
||||||
id: process
|
id: process
|
||||||
|
|
||||||
property int index: 0
|
|
||||||
|
|
||||||
stdinEnabled: true
|
stdinEnabled: true
|
||||||
running: !MusicManager.isAllPaused()
|
running: !MusicManager.isAllPaused()
|
||||||
command: ["cava", "-p", "/dev/stdin"]
|
command: ["cava", "-p", "/dev/stdin"]
|
||||||
onExited: {
|
onExited: {
|
||||||
stdinEnabled = true;
|
stdinEnabled = true;
|
||||||
index = 0;
|
|
||||||
values = Array(count).fill(0);
|
values = Array(count).fill(0);
|
||||||
}
|
}
|
||||||
onStarted: {
|
onStarted: {
|
||||||
@@ -56,23 +55,14 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
stdinEnabled = false;
|
stdinEnabled = false;
|
||||||
|
values = Array(count).fill(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout: SplitParser {
|
stdout: SplitParser {
|
||||||
splitMarker: ""
|
|
||||||
onRead: (data) => {
|
onRead: (data) => {
|
||||||
const newValues = Array(count).fill(0);
|
root.values = data.slice(0, -1).split(";").map((v) => {
|
||||||
for (let i = 0; i < values.length; i++) {
|
return parseInt(v, 10) / 100;
|
||||||
newValues[i] = values[i];
|
});
|
||||||
}
|
|
||||||
if (process.index + data.length > count)
|
|
||||||
process.index = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 1) {
|
|
||||||
newValues[i + process.index] = Math.min(data.charCodeAt(i), 128) / 128;
|
|
||||||
}
|
|
||||||
process.index += data.length;
|
|
||||||
values = newValues;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
58
quickshell/Utils/Logger.qml
Normal file
58
quickshell/Utils/Logger.qml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
function _formatMessage(...args) {
|
||||||
|
var t = Time.getFormattedTimestamp()
|
||||||
|
if (args.length > 1) {
|
||||||
|
const maxLength = 14
|
||||||
|
var module = args.shift().substring(0, maxLength).padStart(maxLength, " ")
|
||||||
|
return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" ")
|
||||||
|
} else {
|
||||||
|
return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getStackTrace() {
|
||||||
|
try {
|
||||||
|
throw new Error("Stack trace")
|
||||||
|
} catch (e) {
|
||||||
|
return e.stack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(...args) {
|
||||||
|
var msg = _formatMessage(...args)
|
||||||
|
console.log(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
function warn(...args) {
|
||||||
|
var msg = _formatMessage(...args)
|
||||||
|
console.warn(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(...args) {
|
||||||
|
var msg = _formatMessage(...args)
|
||||||
|
console.error(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
function callStack() {
|
||||||
|
var stack = _getStackTrace()
|
||||||
|
Logger.log("Debug", "--------------------------")
|
||||||
|
Logger.log("Debug", "Current call stack")
|
||||||
|
// Split the stack into lines and log each one
|
||||||
|
var stackLines = stack.split('\n')
|
||||||
|
for (var i = 0; i < stackLines.length; i++) {
|
||||||
|
var line = stackLines[i].trim() // Remove leading/trailing whitespace
|
||||||
|
if (line.length > 0) {
|
||||||
|
// Only log non-empty lines
|
||||||
|
Logger.log("Debug", `- ${line}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logger.log("Debug", "--------------------------")
|
||||||
|
}
|
||||||
|
}
|
||||||
84
quickshell/Utils/Time.qml
Normal file
84
quickshell/Utils/Time.qml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Current date
|
||||||
|
property var date: new Date()
|
||||||
|
// Returns a Unix Timestamp (in seconds)
|
||||||
|
readonly property int timestamp: {
|
||||||
|
return Math.floor(date / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats a Date object into a YYYYMMDD-HHMMSS string.
|
||||||
|
function getFormattedTimestamp(date) {
|
||||||
|
if (!date)
|
||||||
|
date = new Date();
|
||||||
|
|
||||||
|
const year = date.getFullYear();
|
||||||
|
// getMonth() is zero-based, so we add 1
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format an easy to read approximate duration ex: 4h32m
|
||||||
|
// Used to display the time remaining on the Battery widget, computer uptime, etc..
|
||||||
|
function formatVagueHumanReadableDuration(totalSeconds) {
|
||||||
|
if (typeof totalSeconds !== 'number' || totalSeconds < 0)
|
||||||
|
return '0s';
|
||||||
|
|
||||||
|
// Floor the input to handle decimal seconds
|
||||||
|
totalSeconds = Math.floor(totalSeconds);
|
||||||
|
const days = Math.floor(totalSeconds / 86400);
|
||||||
|
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
const parts = [];
|
||||||
|
if (days)
|
||||||
|
parts.push(`${days}d`);
|
||||||
|
|
||||||
|
if (hours)
|
||||||
|
parts.push(`${hours}h`);
|
||||||
|
|
||||||
|
if (minutes)
|
||||||
|
parts.push(`${minutes}m`);
|
||||||
|
|
||||||
|
// Only show seconds if no hours and no minutes
|
||||||
|
if (!hours && !minutes)
|
||||||
|
parts.push(`${seconds}s`);
|
||||||
|
|
||||||
|
return parts.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format a date into
|
||||||
|
function formatRelativeTime(date) {
|
||||||
|
if (!date)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
const diff = Date.now() - date.getTime();
|
||||||
|
if (diff < 60000)
|
||||||
|
return "now";
|
||||||
|
|
||||||
|
if (diff < 3.6e+06)
|
||||||
|
return `${Math.floor(diff / 60000)}m ago`;
|
||||||
|
|
||||||
|
if (diff < 8.64e+07)
|
||||||
|
return `${Math.floor(diff / 3600000)}h ago`;
|
||||||
|
|
||||||
|
return `${Math.floor(diff / 86400000)}d ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
running: true
|
||||||
|
onTriggered: root.date = new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,10 +4,16 @@ import Quickshell.Widgets
|
|||||||
import qs.Constants
|
import qs.Constants
|
||||||
import qs.Modules.Bar
|
import qs.Modules.Bar
|
||||||
import qs.Modules.Misc
|
import qs.Modules.Misc
|
||||||
|
import qs.Modules.Panel
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Scope {
|
Scope {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
IPCService {
|
||||||
|
id: ipcService
|
||||||
|
}
|
||||||
|
|
||||||
Bar {
|
Bar {
|
||||||
id: bar
|
id: bar
|
||||||
|
|
||||||
@@ -20,4 +26,16 @@ Scope {
|
|||||||
shell: root
|
shell: root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CalendarPanel {
|
||||||
|
id: calendarPanel
|
||||||
|
|
||||||
|
objectName: "calendarPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlCenterPanel {
|
||||||
|
id: controlCenterPanel
|
||||||
|
|
||||||
|
objectName: "controlCenterPanel"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
peach: #fab387;
|
peach: #fab387;
|
||||||
search: rgba(49, 50, 68, 0.5); // alpha(@surface, 0.5)
|
search: rgba(49, 50, 68, 0.5); // alpha(@surface, 0.5)
|
||||||
|
|
||||||
accent: #89b4fa;
|
primary: #89b4fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ window {
|
|||||||
padding: 0px;
|
padding: 0px;
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
border-color: @accent;
|
border-color: @primary;
|
||||||
cursor: "default";
|
cursor: "default";
|
||||||
background-color: @base;
|
background-color: @base;
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,7 @@ listview {
|
|||||||
}
|
}
|
||||||
scrollbar {
|
scrollbar {
|
||||||
handle-width: 5px ;
|
handle-width: 5px ;
|
||||||
handle-color: @accent;
|
handle-color: @primary;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: @mantle;
|
background-color: @mantle;
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ element alternate.normal {
|
|||||||
element normal.urgent,
|
element normal.urgent,
|
||||||
element alternate.urgent,
|
element alternate.urgent,
|
||||||
element selected.active {
|
element selected.active {
|
||||||
background-color: @accent;
|
background-color: @primary;
|
||||||
text-color: @base;
|
text-color: @base;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
@@ -198,9 +198,9 @@ button {
|
|||||||
}
|
}
|
||||||
button selected {
|
button selected {
|
||||||
background-color: @surface;
|
background-color: @surface;
|
||||||
text-color: @accent;
|
text-color: @primary;
|
||||||
border-radius: 0 0 14px 14px;
|
border-radius: 0 0 14px 14px;
|
||||||
border-color: @accent;
|
border-color: @primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*****----- Message -----*****/
|
/*****----- Message -----*****/
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
peach: #fab387;
|
peach: #fab387;
|
||||||
search: rgba(49, 50, 68, 0.5); // alpha(@surface, 0.5)
|
search: rgba(49, 50, 68, 0.5); // alpha(@surface, 0.5)
|
||||||
|
|
||||||
accent: #<FLAVOR_HEX>;
|
primary: #<FLAVOR_HEX>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ window {
|
|||||||
padding: 0px;
|
padding: 0px;
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
border-color: @accent;
|
border-color: @primary;
|
||||||
cursor: "default";
|
cursor: "default";
|
||||||
background-color: @base;
|
background-color: @base;
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,7 @@ listview {
|
|||||||
}
|
}
|
||||||
scrollbar {
|
scrollbar {
|
||||||
handle-width: 5px ;
|
handle-width: 5px ;
|
||||||
handle-color: @accent;
|
handle-color: @primary;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: @mantle;
|
background-color: @mantle;
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ element alternate.normal {
|
|||||||
element normal.urgent,
|
element normal.urgent,
|
||||||
element alternate.urgent,
|
element alternate.urgent,
|
||||||
element selected.active {
|
element selected.active {
|
||||||
background-color: @accent;
|
background-color: @primary;
|
||||||
text-color: @base;
|
text-color: @base;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
@@ -198,9 +198,9 @@ button {
|
|||||||
}
|
}
|
||||||
button selected {
|
button selected {
|
||||||
background-color: @surface;
|
background-color: @surface;
|
||||||
text-color: @accent;
|
text-color: @primary;
|
||||||
border-radius: 0 0 14px 14px;
|
border-radius: 0 0 14px 14px;
|
||||||
border-color: @accent;
|
border-color: @primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*****----- Message -----*****/
|
/*****----- Message -----*****/
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
{
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Global configuration
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
"layer": "bottom",
|
|
||||||
"position": "top",
|
|
||||||
"margin-left": 0,
|
|
||||||
"margin-bottom": 0,
|
|
||||||
"margin-right": 0,
|
|
||||||
"spacing": 2, // Gaps between modules (px)
|
|
||||||
"modules-left": [
|
|
||||||
"custom/rofi",
|
|
||||||
"custom/separator",
|
|
||||||
"group/workspaceactions",
|
|
||||||
"custom/separator",
|
|
||||||
"niri/window",
|
|
||||||
"custom/mediaplayer"
|
|
||||||
],
|
|
||||||
"modules-center": ["clock"],
|
|
||||||
"modules-right": ["group/monitors", "custom/separator", "group/tray-expander", "idle_inhibitor", "custom/power"],
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Modules
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Separators
|
|
||||||
"custom/separator": {
|
|
||||||
"format": "|"
|
|
||||||
},
|
|
||||||
// Buttons
|
|
||||||
"custom/power": {
|
|
||||||
"format": "",
|
|
||||||
"tooltip": false,
|
|
||||||
"on-click": "wlogout",
|
|
||||||
"min-length": 2,
|
|
||||||
"max-length": 2
|
|
||||||
},
|
|
||||||
"custom/rofi": {
|
|
||||||
"format": "",
|
|
||||||
"tooltip": false,
|
|
||||||
// "on-click-right": "fuzzel -l 0 -p '>> ' | xargs -r sh -c",
|
|
||||||
// "on-click": "fuzzel",
|
|
||||||
// "on-click-middle": "pkill -9 fuzzel",
|
|
||||||
"on-click": "eww open main --toggle",
|
|
||||||
"on-click-right": "pkill rofi || rofi -show drun",
|
|
||||||
"min-length": 2,
|
|
||||||
"max-length": 2
|
|
||||||
},
|
|
||||||
"idle_inhibitor": {
|
|
||||||
"format": "{icon}",
|
|
||||||
"format-icons": {
|
|
||||||
"activated": "",
|
|
||||||
"deactivated": ""
|
|
||||||
},
|
|
||||||
"min-length": 2,
|
|
||||||
"max-length": 2
|
|
||||||
},
|
|
||||||
"custom/caffeine": {
|
|
||||||
"format": "{icon}",
|
|
||||||
"format-icons": {
|
|
||||||
"active": "",
|
|
||||||
"inactive": ""
|
|
||||||
},
|
|
||||||
"return-type": "json",
|
|
||||||
"interval": "once",
|
|
||||||
"exec": "$HOME/.config/waybar/modules/caffeine.sh",
|
|
||||||
"on-click": "$HOME/.config/waybar/modules/caffeine.sh toggle && sleep 0.2",
|
|
||||||
"exec-on-event": true,
|
|
||||||
"tooltip": false,
|
|
||||||
"min-length": 2,
|
|
||||||
"max-length": 2
|
|
||||||
},
|
|
||||||
// Time and Date
|
|
||||||
"clock": {
|
|
||||||
"format": "{:%H:%M | %e %b}",
|
|
||||||
"tooltip-format": "<big>{:%Y %B}</big>\n<tt>{calendar}</tt>",
|
|
||||||
"today-format": "<b>{}</b>",
|
|
||||||
"on-click": "niri msg action center-column",
|
|
||||||
"on-scroll-up": "niri msg action set-column-width +10%",
|
|
||||||
"on-scroll-down": "niri msg action set-column-width -10%",
|
|
||||||
"on-click-middle": "niri msg action close-window"
|
|
||||||
},
|
|
||||||
|
|
||||||
// System monitors
|
|
||||||
"group/monitors": {
|
|
||||||
"modules": ["network#speed", "custom/publicip", "temperature", "memory", "cpu", "battery", "backlight", "wireplumber"],
|
|
||||||
"orientation": "inherit"
|
|
||||||
},
|
|
||||||
"network#speed": {
|
|
||||||
"interval": 1,
|
|
||||||
"format": "{ifname}",
|
|
||||||
"format-wifi": " {bandwidthDownBytes} {bandwidthUpBytes} ",
|
|
||||||
"format-ethernet": " {bandwidthDownBytes} {bandwidthUpBytes} ",
|
|
||||||
"format-disconnected": "",
|
|
||||||
"tooltip-format": "{ipaddr}",
|
|
||||||
"format-linked": " {ifname} (No IP)",
|
|
||||||
"tooltip-format-wifi": "{essid} {signalStrength}%",
|
|
||||||
"tooltip-format-ethernet": "{ifname} ",
|
|
||||||
"tooltip-format-disconnected": " Disconnected",
|
|
||||||
"min-length": 20
|
|
||||||
},
|
|
||||||
"custom/publicip": {
|
|
||||||
"interval": 30,
|
|
||||||
"return-type": "json",
|
|
||||||
"format": " {text}",
|
|
||||||
"tooltip-format": "{alt}",
|
|
||||||
"max-length": 6,
|
|
||||||
"min-length": 6,
|
|
||||||
"exec": "$HOME/.config/waybar/modules/publicip.sh",
|
|
||||||
"on-click": "rm -f $HOME/.config/waybar/modules/publicip.cache && sleep 0.1"
|
|
||||||
},
|
|
||||||
"temperature": {
|
|
||||||
"interval": 5,
|
|
||||||
"thermal-zone": 6,
|
|
||||||
"hwmon-path": "/sys/class/hwmon/hwmon6/temp1_input",
|
|
||||||
"critical-threshold": 80,
|
|
||||||
// "format-critical": " {temperatureC}°C",
|
|
||||||
"format-critical": " {temperatureC}°C",
|
|
||||||
"format": "{icon} {temperatureC}°C",
|
|
||||||
"format-icons": ["", "", ""],
|
|
||||||
"max-length": 6,
|
|
||||||
"min-length": 6
|
|
||||||
},
|
|
||||||
"memory": {
|
|
||||||
"interval": 11,
|
|
||||||
// "format": " {used:0.2f} / {total:0.0f} GB",
|
|
||||||
"format": " {percentage}%",
|
|
||||||
"on-click": "killall btop || ghostty -e btop",
|
|
||||||
"max-length": 6,
|
|
||||||
"min-length": 6
|
|
||||||
},
|
|
||||||
"cpu": {
|
|
||||||
"interval": 3,
|
|
||||||
//"format": " {}%", // Icon: microchip
|
|
||||||
"format": " {usage}%",
|
|
||||||
"max-length": 6,
|
|
||||||
"min-length": 6,
|
|
||||||
"on-click": "killall btop || ghostty -e btop"
|
|
||||||
},
|
|
||||||
"battery": {
|
|
||||||
"interval": 30,
|
|
||||||
"states": {
|
|
||||||
"good": 95,
|
|
||||||
"warning": 30,
|
|
||||||
"critical": 15
|
|
||||||
},
|
|
||||||
"format": "{icon} {capacity}%",
|
|
||||||
"format-charging": " {capacity}%",
|
|
||||||
"format-plugged": " {capacity}%",
|
|
||||||
"format-icons": ["", "", "", "", ""],
|
|
||||||
"max-length": 6,
|
|
||||||
"min-length": 6
|
|
||||||
},
|
|
||||||
"backlight": {
|
|
||||||
"device": "$DISPLAY_DEVICE",
|
|
||||||
"format": "{icon} {percent}%",
|
|
||||||
"format-alt": "{percent}% {icon}",
|
|
||||||
"format-alt-click": "click-right",
|
|
||||||
//"format-icons": ["", ""],
|
|
||||||
"format-icons": [""],
|
|
||||||
"on-scroll-down": "brightnessctl -d $HYPR_DISPLAY_DEVICE set 5%-",
|
|
||||||
"on-scroll-up": "brightnessctl -d $HYPR_DISPLAY_DEVICE set +5%",
|
|
||||||
"max-length": 6,
|
|
||||||
"min-length": 6
|
|
||||||
},
|
|
||||||
"wireplumber": {
|
|
||||||
"on-click": "pavucontrol",
|
|
||||||
//on-click: "${wpctl} set-mute @DEFAULT_AUDIO_SINK@ toggle";
|
|
||||||
"on-scroll-down": "wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 0.04-",
|
|
||||||
"on-scroll-up": "wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 0.04+",
|
|
||||||
"format": "{icon} {volume}%",
|
|
||||||
"format-muted": "",
|
|
||||||
"format-source": "",
|
|
||||||
"format-source-muted": "",
|
|
||||||
//"format-muted": "<span foreground='#fab387'> </span>",
|
|
||||||
//"format-icons": [ "<span foreground='#fab387'></span>" ]
|
|
||||||
"format-icons": {
|
|
||||||
"headphone": "",
|
|
||||||
"phone": "",
|
|
||||||
"portable": "",
|
|
||||||
"car": "",
|
|
||||||
"default": ["", "", "", "", "", ""]
|
|
||||||
},
|
|
||||||
"max-length": 6,
|
|
||||||
"min-length": 6
|
|
||||||
},
|
|
||||||
// Niri
|
|
||||||
"group/workspaceactions": {
|
|
||||||
"modules": ["niri/workspaces", "custom/workspacenew"],
|
|
||||||
"orientation": "inherit"
|
|
||||||
},
|
|
||||||
"niri/workspaces": {
|
|
||||||
"all-outputs": true,
|
|
||||||
"format": "{index}",
|
|
||||||
"on-scroll-up": "niri msg action focus-workspace-up",
|
|
||||||
"on-scroll-down": "niri msg action focus-workspace-down",
|
|
||||||
"sort-by-number": true
|
|
||||||
},
|
|
||||||
"niri/window": {
|
|
||||||
"format": "",
|
|
||||||
"separate-outputs": true,
|
|
||||||
"icon": true,
|
|
||||||
"icon-size": 14
|
|
||||||
},
|
|
||||||
"custom/mediaplayer": {
|
|
||||||
"format": "{text}",
|
|
||||||
"return-type": "json",
|
|
||||||
"max-length": 100,
|
|
||||||
"escape": true,
|
|
||||||
"exec": "$HOME/.config/waybar/modules/mediaplayer.py 2> /dev/null",
|
|
||||||
"on-click": "playerctl play-pause",
|
|
||||||
"on-click-right": "lyrics-widgets",
|
|
||||||
"on-scroll-up": "playerctl next",
|
|
||||||
"on-scroll-down": "playerctl previous"
|
|
||||||
},
|
|
||||||
"group/tray-expander": {
|
|
||||||
"orientation": "inherit",
|
|
||||||
"drawer": {
|
|
||||||
"transition-duration": 600,
|
|
||||||
"children-class": "tray-group-item"
|
|
||||||
},
|
|
||||||
"modules": ["custom/expand-icon", "tray", "custom/separator"]
|
|
||||||
},
|
|
||||||
"custom/expand-icon": {
|
|
||||||
"format": "",
|
|
||||||
"tooltip": false,
|
|
||||||
"min-length": 2,
|
|
||||||
"max-length": 2
|
|
||||||
},
|
|
||||||
"tray": {
|
|
||||||
"icon-size": 15,
|
|
||||||
"spacing": 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
@define-color rosewater #f5e0dc;
|
|
||||||
@define-color flamingo #f2cdcd;
|
|
||||||
@define-color pink #f5c2e7;
|
|
||||||
@define-color mauve #cba6f7;
|
|
||||||
@define-color red #f38ba8;
|
|
||||||
@define-color maroon #eba0ac;
|
|
||||||
@define-color peach #fab387;
|
|
||||||
@define-color yellow #f9e2af;
|
|
||||||
@define-color green #a6e3a1;
|
|
||||||
@define-color teal #94e2d5;
|
|
||||||
@define-color sky #89dceb;
|
|
||||||
@define-color sapphire #74c7ec;
|
|
||||||
@define-color blue #89b4fa;
|
|
||||||
@define-color lavender #b4befe;
|
|
||||||
@define-color text #cdd6f4;
|
|
||||||
@define-color subtext1 #bac2de;
|
|
||||||
@define-color subtext0 #a6adc8;
|
|
||||||
@define-color overlay2 #9399b2;
|
|
||||||
@define-color overlay1 #7f849c;
|
|
||||||
@define-color overlay0 #6c7086;
|
|
||||||
@define-color surface2 #585b70;
|
|
||||||
@define-color surface1 #45475a;
|
|
||||||
@define-color surface0 #313244;
|
|
||||||
@define-color base #1e1e2e;
|
|
||||||
@define-color mantle #181825;
|
|
||||||
@define-color crust #11111b;
|
|
||||||
3
waybar-niri/modules/.gitignore
vendored
3
waybar-niri/modules/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
publicip.conf
|
|
||||||
publicip.cache
|
|
||||||
publicip.log
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#!/bin/env bash
|
|
||||||
|
|
||||||
function output() {
|
|
||||||
jq -n --unbuffered --compact-output \
|
|
||||||
--arg alt "$1" \
|
|
||||||
--arg class "$2" \
|
|
||||||
'{alt: $alt, class: [$class]}'
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "$1" = "toggle" ]; then
|
|
||||||
pid=$(pgrep -x "swayidle")
|
|
||||||
|
|
||||||
if [ -n "$pid" ]; then
|
|
||||||
killall swayidle > /dev/null 2>&1
|
|
||||||
notify-send "Caffeine enabled" "POWERRR!!!"
|
|
||||||
else
|
|
||||||
nohup niri-swayidle > /dev/null 2>&1 &
|
|
||||||
notify-send "Caffeine disabled" "zzz..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# sleep 0.2 # Allow hypridle to start
|
|
||||||
|
|
||||||
pid=$(pgrep -x "swayidle")
|
|
||||||
if [ -n "$pid" ]; then
|
|
||||||
output "inactive" "inactive"
|
|
||||||
else
|
|
||||||
output "active" "active"
|
|
||||||
fi
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
from gi.repository.Playerctl import Player
|
|
||||||
from gi.repository import Playerctl, GLib
|
|
||||||
from typing import List
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import signal
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Playerctl", "2.0")
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(sig, frame):
|
|
||||||
logger.info("Received signal to stop, exiting")
|
|
||||||
sys.stdout.write("\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
# loop.quit()
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerManager:
|
|
||||||
def __init__(self, selected_player=None, excluded_player=[]):
|
|
||||||
self.manager = Playerctl.PlayerManager()
|
|
||||||
self.loop = GLib.MainLoop()
|
|
||||||
self.manager.connect(
|
|
||||||
"name-appeared", lambda *args: self.on_player_appeared(*args)
|
|
||||||
)
|
|
||||||
self.manager.connect(
|
|
||||||
"player-vanished", lambda *args: self.on_player_vanished(*args)
|
|
||||||
)
|
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
|
||||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
|
||||||
self.selected_player = selected_player
|
|
||||||
self.excluded_player = excluded_player.split(",") if excluded_player else []
|
|
||||||
|
|
||||||
self.init_players()
|
|
||||||
|
|
||||||
def init_players(self):
|
|
||||||
for player in self.manager.props.player_names:
|
|
||||||
if player.name in self.excluded_player:
|
|
||||||
continue
|
|
||||||
if self.selected_player is not None and self.selected_player != player.name:
|
|
||||||
logger.debug(f"{player.name} is not the filtered player, skipping it")
|
|
||||||
continue
|
|
||||||
self.init_player(player)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
logger.info("Starting main loop")
|
|
||||||
self.loop.run()
|
|
||||||
|
|
||||||
def init_player(self, player):
|
|
||||||
logger.info(f"Initialize new player: {player.name}")
|
|
||||||
player = Playerctl.Player.new_from_name(player)
|
|
||||||
player.connect("playback-status", self.on_playback_status_changed, None)
|
|
||||||
player.connect("metadata", self.on_metadata_changed, None)
|
|
||||||
self.manager.manage_player(player)
|
|
||||||
self.on_metadata_changed(player, player.props.metadata)
|
|
||||||
|
|
||||||
def get_players(self) -> List[Player]:
|
|
||||||
return self.manager.props.players
|
|
||||||
|
|
||||||
def write_output(self, text, player):
|
|
||||||
logger.debug(f"Writing output: {text}")
|
|
||||||
|
|
||||||
output = {
|
|
||||||
"text": text,
|
|
||||||
"class": "custom-" + player.props.player_name,
|
|
||||||
"alt": player.props.player_name,
|
|
||||||
}
|
|
||||||
|
|
||||||
sys.stdout.write(json.dumps(output) + "\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
def clear_output(self):
|
|
||||||
sys.stdout.write("\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
def on_playback_status_changed(self, player, status, _=None):
|
|
||||||
logger.debug(
|
|
||||||
f"Playback status changed for player {player.props.player_name}: {status}"
|
|
||||||
)
|
|
||||||
self.on_metadata_changed(player, player.props.metadata)
|
|
||||||
|
|
||||||
def get_first_playing_player(self):
|
|
||||||
players = self.get_players()
|
|
||||||
logger.debug(f"Getting first playing player from {len(players)} players")
|
|
||||||
if len(players) > 0:
|
|
||||||
# if any are playing, show the first one that is playing
|
|
||||||
# reverse order, so that the most recently added ones are preferred
|
|
||||||
for player in players[::-1]:
|
|
||||||
if player.props.status == "Playing":
|
|
||||||
return player
|
|
||||||
# if none are playing, show the first one
|
|
||||||
return players[0]
|
|
||||||
else:
|
|
||||||
logger.debug("No players found")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def show_most_important_player(self):
|
|
||||||
logger.debug("Showing most important player")
|
|
||||||
# show the currently playing player
|
|
||||||
# or else show the first paused player
|
|
||||||
# or else show nothing
|
|
||||||
current_player = self.get_first_playing_player()
|
|
||||||
if current_player is not None:
|
|
||||||
self.on_metadata_changed(current_player, current_player.props.metadata)
|
|
||||||
else:
|
|
||||||
self.clear_output()
|
|
||||||
|
|
||||||
def on_metadata_changed(self, player, metadata, _=None):
|
|
||||||
logger.debug(f"Metadata changed for player {player.props.player_name}")
|
|
||||||
player_name = player.props.player_name
|
|
||||||
artist = player.get_artist()
|
|
||||||
title = player.get_title()
|
|
||||||
title = title.replace("&", "&")
|
|
||||||
|
|
||||||
track_info = ""
|
|
||||||
if (
|
|
||||||
player_name == "spotify"
|
|
||||||
and "mpris:trackid" in metadata.keys()
|
|
||||||
and ":ad:" in player.props.metadata["mpris:trackid"]
|
|
||||||
):
|
|
||||||
track_info = "Advertisement"
|
|
||||||
elif artist is not None and title is not None:
|
|
||||||
track_info = f"{artist} - {title}"
|
|
||||||
else:
|
|
||||||
track_info = title
|
|
||||||
|
|
||||||
if track_info:
|
|
||||||
if player.props.status == "Playing":
|
|
||||||
track_info = " " + track_info
|
|
||||||
else:
|
|
||||||
track_info = " " + track_info
|
|
||||||
# only print output if no other player is playing
|
|
||||||
current_playing = self.get_first_playing_player()
|
|
||||||
if (
|
|
||||||
current_playing is None
|
|
||||||
or current_playing.props.player_name == player.props.player_name
|
|
||||||
):
|
|
||||||
self.write_output(track_info, player)
|
|
||||||
else:
|
|
||||||
logger.debug(
|
|
||||||
f"Other player {current_playing.props.player_name} is playing, skipping"
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_player_appeared(self, _, player):
|
|
||||||
logger.info(f"Player has appeared: {player.name}")
|
|
||||||
if player.name in self.excluded_player:
|
|
||||||
logger.debug(
|
|
||||||
"New player appeared, but it's in exclude player list, skipping"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if player is not None and (
|
|
||||||
self.selected_player is None or player.name == self.selected_player
|
|
||||||
):
|
|
||||||
self.init_player(player)
|
|
||||||
else:
|
|
||||||
logger.debug(
|
|
||||||
"New player appeared, but it's not the selected player, skipping"
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_player_vanished(self, _, player):
|
|
||||||
logger.info(f"Player {player.props.player_name} has vanished")
|
|
||||||
self.show_most_important_player()
|
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments():
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
|
|
||||||
# Increase verbosity with every occurrence of -v
|
|
||||||
parser.add_argument("-v", "--verbose", action="count", default=0)
|
|
||||||
|
|
||||||
parser.add_argument("-x", "--exclude", "- Comma-separated list of excluded player")
|
|
||||||
|
|
||||||
# Define for which player we"re listening
|
|
||||||
parser.add_argument("--player")
|
|
||||||
|
|
||||||
parser.add_argument("--enable-logging", action="store_true")
|
|
||||||
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
arguments = parse_arguments()
|
|
||||||
|
|
||||||
# Initialize logging
|
|
||||||
if arguments.enable_logging:
|
|
||||||
logfile = os.path.join(
|
|
||||||
os.path.dirname(os.path.realpath(__file__)), "media-player.log"
|
|
||||||
)
|
|
||||||
logging.basicConfig(
|
|
||||||
filename=logfile,
|
|
||||||
level=logging.DEBUG,
|
|
||||||
format="%(asctime)s %(name)s %(levelname)s:%(lineno)d %(message)s",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Logging is set by default to WARN and higher.
|
|
||||||
# With every occurrence of -v it's lowered by one
|
|
||||||
logger.setLevel(max((3 - arguments.verbose) * 10, 0))
|
|
||||||
|
|
||||||
logger.info("Creating player manager")
|
|
||||||
if arguments.player:
|
|
||||||
logger.info(f"Filtering for player: {arguments.player}")
|
|
||||||
if arguments.exclude:
|
|
||||||
logger.info(f"Exclude player {arguments.exclude}")
|
|
||||||
|
|
||||||
player = PlayerManager(arguments.player, arguments.exclude)
|
|
||||||
player.run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# shellcheck disable=SC1091,SC1090
|
|
||||||
|
|
||||||
# Entries in publicip.conf:
|
|
||||||
# IP_QUERY_URL: URL to query public IP of the system.
|
|
||||||
# return: JSON object with "ip" field.
|
|
||||||
# note: This URL will be queried with short intervals (60s for example),
|
|
||||||
# therefore it may not be a good idea to use a public API with
|
|
||||||
# a limited number of calls.
|
|
||||||
# IP_INFO_URL: URL to query IP location.
|
|
||||||
# placeholder: <ip>
|
|
||||||
# return: JSON object with "country_code" field.
|
|
||||||
# note: This URL will only be quetried when public IP changes or when "force" is given as parameter.
|
|
||||||
|
|
||||||
path="$(dirname "$(readlink -f "$0")")"
|
|
||||||
cache_file="$path/publicip.cache"
|
|
||||||
config_file="$path/publicip.conf"
|
|
||||||
time_log="$path/publicip.log"
|
|
||||||
|
|
||||||
[ -f "$config_file" ] || exit 1
|
|
||||||
. "$config_file"
|
|
||||||
[ -z "$IP_QUERY_URL" ] && exit 1
|
|
||||||
[ -z "$IP_INFO_URL" ] && exit 1
|
|
||||||
[ "$1" == "force" ] && rm -f "$cache_file"
|
|
||||||
[ -f "$cache_file" ] && . "$cache_file"
|
|
||||||
|
|
||||||
# Try to check network connectivity before querying
|
|
||||||
if ! ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1; then
|
|
||||||
# No network, return cached values if available
|
|
||||||
[ -z "$CACHED_IP" ] && CACHED_IP="N/A"
|
|
||||||
[ -z "$CACHED_CODE" ] && CACHED_CODE="N/A"
|
|
||||||
|
|
||||||
echo "$(date +%Y-%m-%dT%H:%M:%S) - Waiting for network" >>"$time_log"
|
|
||||||
|
|
||||||
jq -n --unbuffered --compact-output \
|
|
||||||
--arg ip "$CACHED_IP" \
|
|
||||||
--arg country "$CACHED_CODE" \
|
|
||||||
'{alt: $ip, text: $country}'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
ip_current=$(curl -s -L -4 "$IP_QUERY_URL" | jq -r '.ip')
|
|
||||||
[ -z "$ip_current" ] && exit 1
|
|
||||||
|
|
||||||
if [ "$ip_current" != "$CACHED_IP" ]; then
|
|
||||||
echo "$(date +%Y-%m-%dT%H:%M:%S) - IP changed: $CACHED_IP -> $ip_current" >>"$time_log"
|
|
||||||
CACHED_IP="$ip_current"
|
|
||||||
|
|
||||||
ip_info_url=${IP_INFO_URL//<ip>/$ip_current}
|
|
||||||
CACHED_CODE=$(curl -s -L "$ip_info_url" | jq -r '.country_code')
|
|
||||||
[ -z "$CACHED_CODE" ] && CACHED_CODE="N/A"
|
|
||||||
|
|
||||||
echo "CACHED_IP=$CACHED_IP" >"$cache_file"
|
|
||||||
echo "CACHED_CODE=$CACHED_CODE" >>"$cache_file"
|
|
||||||
notify-send "New Public IP detected" "New IP: $ip_current\nCountry: $CACHED_CODE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
jq -n --unbuffered --compact-output \
|
|
||||||
--arg ip "$CACHED_IP" \
|
|
||||||
--arg country "$CACHED_CODE" \
|
|
||||||
'{alt: $ip, text: $country}'
|
|
||||||
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
@import 'mocha.css';
|
|
||||||
|
|
||||||
@define-color flavor #89b4fa;
|
|
||||||
/* @define-color archlinux #1793d1; */
|
|
||||||
@define-color archlinux @sapphire;
|
|
||||||
|
|
||||||
/* Font(s) */
|
|
||||||
* {
|
|
||||||
/* main font icons CJK fallback */
|
|
||||||
font-family: 'Sour Gummy Light', 'Meslo LGM Nerd Font Mono', 'WenQuanYi Micro Hei', 'Noto Sans', sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset all styles */
|
|
||||||
* {
|
|
||||||
border: none;
|
|
||||||
border-radius: 14px;
|
|
||||||
min-height: 0;
|
|
||||||
margin: 2px 1px 2px 1px;
|
|
||||||
padding: 0;
|
|
||||||
transition-property: background-color;
|
|
||||||
transition-duration: 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The whole bar */
|
|
||||||
#waybar {
|
|
||||||
background: linear-gradient(to bottom, alpha(@base, 0.8), alpha(@base, 0));
|
|
||||||
/* background: transparent; */
|
|
||||||
color: @text;
|
|
||||||
border-radius: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltip {
|
|
||||||
background: @base;
|
|
||||||
border: 2px solid @overlay0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaceactions,
|
|
||||||
#window,
|
|
||||||
#clock,
|
|
||||||
#monitors,
|
|
||||||
#custom-mediaplayer,
|
|
||||||
#custom-power-menu,
|
|
||||||
#tray,
|
|
||||||
#custom-rofi,
|
|
||||||
#idle_inhibitor,
|
|
||||||
#custom-caffeine,
|
|
||||||
#custom-power {
|
|
||||||
padding: 0px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-separator {
|
|
||||||
padding: 0px 5px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-rofi,
|
|
||||||
#custom-caffeine
|
|
||||||
#idle_inhibitor,
|
|
||||||
#custom-power,
|
|
||||||
#custom-expand-icon {
|
|
||||||
padding: 0px 6px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-workspacenew,
|
|
||||||
#workspaces button {
|
|
||||||
padding: 0px;
|
|
||||||
margin: 3px 3px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: @flavor;
|
|
||||||
background-color: transparent;
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces button:hover {
|
|
||||||
background-color: alpha(@flavor, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces button.active {
|
|
||||||
color: @base;
|
|
||||||
background: @flavor;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces button.urgent {
|
|
||||||
color: @base;
|
|
||||||
background-color: @red;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaceactions {
|
|
||||||
padding-left: 1px;
|
|
||||||
padding-right: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces {
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#window {
|
|
||||||
transition: none; /* Disable background transition */
|
|
||||||
}
|
|
||||||
|
|
||||||
window#waybar.empty #window {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-mediaplayer {
|
|
||||||
color: @flavor;
|
|
||||||
}
|
|
||||||
|
|
||||||
#network.speed {
|
|
||||||
color: @flavor;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-publicip {
|
|
||||||
color: @peach;
|
|
||||||
}
|
|
||||||
|
|
||||||
#temperature {
|
|
||||||
color: @yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
#memory {
|
|
||||||
color: @green;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cpu {
|
|
||||||
color: @teal;
|
|
||||||
}
|
|
||||||
|
|
||||||
#battery {
|
|
||||||
color: @sapphire;
|
|
||||||
}
|
|
||||||
|
|
||||||
#battery.charging,
|
|
||||||
#battery.full,
|
|
||||||
#battery.plugged {
|
|
||||||
color: @green;
|
|
||||||
}
|
|
||||||
|
|
||||||
#backlight {
|
|
||||||
color: @blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
#wireplumber {
|
|
||||||
color: @lavender;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-power {
|
|
||||||
color: @maroon;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-power:hover {
|
|
||||||
background-color: alpha(@maroon, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-rofi {
|
|
||||||
color: @archlinux;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-rofi:hover {
|
|
||||||
background-color: alpha(@archlinux, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-expand-icon {
|
|
||||||
color: @green;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-caffeine,
|
|
||||||
#idle_inhibitor {
|
|
||||||
color: @yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-caffeine:hover,
|
|
||||||
#idle_inhibitor:hover {
|
|
||||||
background-color: alpha(@yellow, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-caffeine.active,
|
|
||||||
#idle_inhibitor.activated {
|
|
||||||
color: @peach;
|
|
||||||
}
|
|
||||||
|
|
||||||
#clock {
|
|
||||||
color: @flavor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink {
|
|
||||||
to {
|
|
||||||
background-color: alpha(@red, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#battery.critical:not(.charging) {
|
|
||||||
color: @red;
|
|
||||||
animation-name: blink;
|
|
||||||
animation-duration: 1s;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-direction: alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
#network.disconnected {
|
|
||||||
color: @red;
|
|
||||||
}
|
|
||||||
|
|
||||||
#temperature.critical {
|
|
||||||
background-color: #eb4d4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tray > .passive {
|
|
||||||
-gtk-icon-effect: dim;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tray > .needs-attention {
|
|
||||||
-gtk-icon-effect: highlight;
|
|
||||||
background-color: #eb4d4b;
|
|
||||||
}
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
@import 'mocha.css';
|
|
||||||
|
|
||||||
@define-color flavor #<FLAVOR_HEX>;
|
|
||||||
/* @define-color archlinux #1793d1; */
|
|
||||||
@define-color archlinux @sapphire;
|
|
||||||
|
|
||||||
/* Font(s) */
|
|
||||||
* {
|
|
||||||
/* main font icons CJK fallback */
|
|
||||||
font-family: 'Sour Gummy Light', 'Meslo LGM Nerd Font Mono', 'WenQuanYi Micro Hei', 'Noto Sans', sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset all styles */
|
|
||||||
* {
|
|
||||||
border: none;
|
|
||||||
border-radius: 14px;
|
|
||||||
min-height: 0;
|
|
||||||
margin: 2px 1px 2px 1px;
|
|
||||||
padding: 0;
|
|
||||||
transition-property: background-color;
|
|
||||||
transition-duration: 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The whole bar */
|
|
||||||
#waybar {
|
|
||||||
background: linear-gradient(to bottom, alpha(@base, 0.8), alpha(@base, 0));
|
|
||||||
/* background: transparent; */
|
|
||||||
color: @text;
|
|
||||||
border-radius: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltip {
|
|
||||||
background: @base;
|
|
||||||
border: 2px solid @overlay0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaceactions,
|
|
||||||
#window,
|
|
||||||
#clock,
|
|
||||||
#monitors,
|
|
||||||
#custom-mediaplayer,
|
|
||||||
#custom-power-menu,
|
|
||||||
#tray,
|
|
||||||
#custom-rofi,
|
|
||||||
#idle_inhibitor,
|
|
||||||
#custom-caffeine,
|
|
||||||
#custom-power {
|
|
||||||
padding: 0px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-separator {
|
|
||||||
padding: 0px 5px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-rofi,
|
|
||||||
#custom-caffeine
|
|
||||||
#idle_inhibitor,
|
|
||||||
#custom-power,
|
|
||||||
#custom-expand-icon {
|
|
||||||
padding: 0px 6px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-workspacenew,
|
|
||||||
#workspaces button {
|
|
||||||
padding: 0px;
|
|
||||||
margin: 3px 3px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: @flavor;
|
|
||||||
background-color: transparent;
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces button:hover {
|
|
||||||
background-color: alpha(@flavor, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces button.active {
|
|
||||||
color: @base;
|
|
||||||
background: @flavor;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces button.urgent {
|
|
||||||
color: @base;
|
|
||||||
background-color: @red;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaceactions {
|
|
||||||
padding-left: 1px;
|
|
||||||
padding-right: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces {
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#window {
|
|
||||||
transition: none; /* Disable background transition */
|
|
||||||
}
|
|
||||||
|
|
||||||
window#waybar.empty #window {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-mediaplayer {
|
|
||||||
color: @flavor;
|
|
||||||
}
|
|
||||||
|
|
||||||
#network.speed {
|
|
||||||
color: @flavor;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-publicip {
|
|
||||||
color: @peach;
|
|
||||||
}
|
|
||||||
|
|
||||||
#temperature {
|
|
||||||
color: @yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
#memory {
|
|
||||||
color: @green;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cpu {
|
|
||||||
color: @teal;
|
|
||||||
}
|
|
||||||
|
|
||||||
#battery {
|
|
||||||
color: @sapphire;
|
|
||||||
}
|
|
||||||
|
|
||||||
#battery.charging,
|
|
||||||
#battery.full,
|
|
||||||
#battery.plugged {
|
|
||||||
color: @green;
|
|
||||||
}
|
|
||||||
|
|
||||||
#backlight {
|
|
||||||
color: @blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
#wireplumber {
|
|
||||||
color: @lavender;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-power {
|
|
||||||
color: @maroon;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-power:hover {
|
|
||||||
background-color: alpha(@maroon, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-rofi {
|
|
||||||
color: @archlinux;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-rofi:hover {
|
|
||||||
background-color: alpha(@archlinux, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-expand-icon {
|
|
||||||
color: @green;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-caffeine,
|
|
||||||
#idle_inhibitor {
|
|
||||||
color: @yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-caffeine:hover,
|
|
||||||
#idle_inhibitor:hover {
|
|
||||||
background-color: alpha(@yellow, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-caffeine.active,
|
|
||||||
#idle_inhibitor.activated {
|
|
||||||
color: @peach;
|
|
||||||
}
|
|
||||||
|
|
||||||
#clock {
|
|
||||||
color: @flavor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink {
|
|
||||||
to {
|
|
||||||
background-color: alpha(@red, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#battery.critical:not(.charging) {
|
|
||||||
color: @red;
|
|
||||||
animation-name: blink;
|
|
||||||
animation-duration: 1s;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-direction: alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
#network.disconnected {
|
|
||||||
color: @red;
|
|
||||||
}
|
|
||||||
|
|
||||||
#temperature.critical {
|
|
||||||
background-color: #eb4d4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tray > .passive {
|
|
||||||
-gtk-icon-effect: dim;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tray > .needs-attention {
|
|
||||||
-gtk-icon-effect: highlight;
|
|
||||||
background-color: #eb4d4b;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# Terminate already running bar instances
|
|
||||||
killall -q waybar
|
|
||||||
|
|
||||||
# Wait until the processes have been shut down
|
|
||||||
while pgrep -x waybar >/dev/null; do sleep 1; done
|
|
||||||
|
|
||||||
# Launch main
|
|
||||||
waybar &
|
|
||||||
Reference in New Issue
Block a user