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
|
||||
|
||||
- niri: edit $HOME/.config/niri/config.kdl
|
||||
|
||||
- quickshell qs ipc call colors setPrimary $hex
|
||||
'''
|
||||
|
||||
import os
|
||||
@@ -216,6 +218,11 @@ def _change_niri(palette: dict[str, str], flavor: str):
|
||||
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]] = {
|
||||
'kvantum': _change_kvantum,
|
||||
# 'nwg-look': _change_nwglook,
|
||||
@@ -230,6 +237,7 @@ apply_theme_funcs: dict[str, Callable[[dict[str, str], str], None]] = {
|
||||
'wlogout': _change_wlogout,
|
||||
'fuzzel': _change_fuzzel,
|
||||
'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('-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('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()
|
||||
|
||||
@@ -363,15 +372,34 @@ def main():
|
||||
return flavor
|
||||
|
||||
def parse_apps() -> list[str]:
|
||||
apps = set()
|
||||
if not arguments.arguments:
|
||||
return list(apply_theme_funcs.keys())
|
||||
apps = []
|
||||
apps = set(apply_theme_funcs.keys())
|
||||
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:
|
||||
if arg not in apply_theme_funcs:
|
||||
print(f"Unknown app: {arg}. Available apps: {', '.join(apply_theme_funcs.keys())}")
|
||||
sys.exit(1)
|
||||
apps.append(arg)
|
||||
return apps
|
||||
if arg[0] == '!':
|
||||
print(f"Excluding app: {arg[1:]}")
|
||||
app = arg[1:]
|
||||
if app not in apply_theme_funcs:
|
||||
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 = PALETTES[palette_name]
|
||||
|
||||
@@ -80,13 +80,13 @@ if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
|
||||
|
||||
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
|
||||
swww img -n background "$image_copied" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
|
||||
|
||||
notify-send "Wallpaper Changed" "$image"
|
||||
|
||||
change-colortheme -i "$image_copied" || exit 1
|
||||
change-colortheme -i "$image_copied" !waybar !eww || exit 1
|
||||
else
|
||||
echo "Unsupported desktop environment: $XDG_CURRENT_DESKTOP"
|
||||
exit 1
|
||||
|
||||
@@ -6,7 +6,7 @@ else
|
||||
desktop="$1"
|
||||
fi
|
||||
|
||||
for item in "waybar" "kitty" "ghostty" "wlogout"; do
|
||||
for item in "kitty" "ghostty" "wlogout"; do
|
||||
[ -L "$HOME/.config/$item" ] || exit 1
|
||||
|
||||
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
|
||||
|
||||
- desktop with a few widgets:
|
||||
<img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/desktop.jpg?raw=true"/>
|
||||
- Hyprland & Waybar & Eww:
|
||||
|
||||
- dynamic colortheme based on Catppuccin Mocha:
|
||||
<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/desktop.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"/>
|
||||
|
||||
## Setup Overview
|
||||
|
||||
- **OS**: Archlinux
|
||||
- **WM**: Niri & Hyprland (looks similar through screenshots)
|
||||
- **Bar**: Waybar
|
||||
- **WM**: Hyprland | Niri
|
||||
- **Bar**: Waybar | Quickshell
|
||||
- **Shell**: Fish
|
||||
- **Prompt**: Oh My Posh
|
||||
- **Terminal**: Kitty & Ghostty
|
||||
- **Colorscheme**: Catppuccin Mocha
|
||||
- **App Launcher**: Rofi
|
||||
- **Logout Screen**: Wlogout
|
||||
- **Desktop Widgets**: Eww
|
||||
- **Desktop Widgets**: Eww | Quickshell
|
||||
- **Wallpaper Darmon**: swww
|
||||
- **Notification Daemon**: Mako
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
@@ -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-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
|
||||
|
||||
Based on [codeopshq/dotfiles](https://github.com/codeopshq/dotfiles), also serves as cliphist browser and emojis picker.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -57,8 +69,8 @@ See [backgrounds repo for personal usage](https://github.com/Uyanide/backgrounds
|
||||
|
||||
including:
|
||||
|
||||
- MesloLGM Nerd Font (& Mono)
|
||||
- Maple Mono NF CN
|
||||
- MesloLGM Nerd Font (& Mono)
|
||||
- WenQuanYi Micro Hei
|
||||
- Sour Gummy
|
||||
- Noto Sans
|
||||
|
||||
Submodule backgrounds updated: 49aad19814...ec1022ffec
@@ -27,7 +27,7 @@ input {
|
||||
warp-mouse-to-focus
|
||||
|
||||
// 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************************/
|
||||
@@ -114,11 +114,6 @@ layer-rule {
|
||||
|
||||
}
|
||||
|
||||
layer-rule {
|
||||
match namespace="^quickshell-bar$"
|
||||
place-within-backdrop false
|
||||
}
|
||||
|
||||
|
||||
/************************Autostart************************/
|
||||
|
||||
@@ -126,7 +121,7 @@ layer-rule {
|
||||
spawn-sh-at-startup "config-switch niri"
|
||||
|
||||
// Bar
|
||||
spawn-at-startup "waybar"
|
||||
spawn-at-startup "quickshell"
|
||||
|
||||
// Wallpaper
|
||||
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 "mako"
|
||||
|
||||
// idle
|
||||
// Idle
|
||||
spawn-sh-at-startup "hypridle"
|
||||
|
||||
// Clipboard history
|
||||
@@ -286,8 +281,8 @@ binds {
|
||||
Mod+Shift+W { spawn "wallpaper-chooser"; }
|
||||
|
||||
// EWW
|
||||
Mod+Space { spawn-sh "eww open main --toggle"; }
|
||||
Mod+Shift+L { spawn-sh "lyrics-widgets"; }
|
||||
Mod+Space { spawn-sh "qs ipc call panels toggleControlCenter"; }
|
||||
Mod+Shift+L { spawn-sh "qs ipc call lyrics toggleBarLyrics"; }
|
||||
|
||||
// Waybar
|
||||
Mod+Shift+K { spawn-sh "waybar-toggle"; }
|
||||
|
||||
@@ -27,7 +27,7 @@ input {
|
||||
warp-mouse-to-focus
|
||||
|
||||
// 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************************/
|
||||
@@ -121,7 +121,7 @@ layer-rule {
|
||||
spawn-sh-at-startup "config-switch niri"
|
||||
|
||||
// Bar
|
||||
spawn-at-startup "waybar"
|
||||
spawn-at-startup "quickshell"
|
||||
|
||||
// Wallpaper
|
||||
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 "mako"
|
||||
|
||||
// idle
|
||||
// Idle
|
||||
spawn-sh-at-startup "hypridle"
|
||||
|
||||
// Clipboard history
|
||||
@@ -281,8 +281,8 @@ binds {
|
||||
Mod+Shift+W { spawn "wallpaper-chooser"; }
|
||||
|
||||
// EWW
|
||||
Mod+Space { spawn-sh "eww open main --toggle"; }
|
||||
Mod+Shift+L { spawn-sh "lyrics-widgets"; }
|
||||
Mod+Space { spawn-sh "qs ipc call panels toggleControlCenter"; }
|
||||
Mod+Shift+L { spawn-sh "qs ipc call lyrics toggleBarLyrics"; }
|
||||
|
||||
// Waybar
|
||||
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 Quickshell
|
||||
import qs.Services
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
@@ -29,10 +30,11 @@ Singleton {
|
||||
readonly property color surface2: "#585b70"
|
||||
readonly property color surface1: "#45475a"
|
||||
readonly property color surface0: "#313244"
|
||||
readonly property color surface: "#292a3c"
|
||||
readonly property color base: "#1e1e2e"
|
||||
readonly property color mantle: "#181825"
|
||||
readonly property color crust: "#11111b"
|
||||
readonly property color accent: "#89b4fa"
|
||||
property color primary: SettingsService.primaryColor
|
||||
readonly property color distroColor: "#74c7ec"
|
||||
readonly property var cavaList: ["#b4befe", "#89b4fa", "#74c7ec", "#89dceb", "#94e2d5", "#a6e3a1", "#f9e2af", "#fab387"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
@@ -8,8 +9,8 @@ Singleton {
|
||||
readonly property string primary: "Sour Gummy Light"
|
||||
readonly property string nerd: "Meslo LGM Nerd Font Mono"
|
||||
readonly property string sans: "Noto Sans"
|
||||
readonly property int small: 10
|
||||
readonly property int medium: 12
|
||||
readonly property int large: 14
|
||||
readonly property int icon: 14
|
||||
readonly property int small: Style.fontSizeS
|
||||
readonly property int medium: Style.fontSizeM
|
||||
readonly property int large: Style.fontSizeL
|
||||
readonly property int icon: 14 // for nerd font
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
@@ -30,6 +31,11 @@ Singleton {
|
||||
readonly property string global: ""
|
||||
readonly property string upload: ""
|
||||
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
|
||||
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
|
||||
readonly property string defaultIcon: TablerIcons.defaultIcon
|
||||
@@ -73,7 +79,7 @@ Singleton {
|
||||
if (currentFontLoader.status === FontLoader.Ready)
|
||||
fontReloaded();
|
||||
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
|
||||
WlrLayershell.namespace: "quickshell-bar"
|
||||
color: Colors.transparent
|
||||
implicitHeight: 45
|
||||
implicitHeight: Style.barHeight
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
@@ -94,6 +94,9 @@ Scope {
|
||||
SymbolButton {
|
||||
symbol: Icons.distro
|
||||
buttonColor: Colors.distroColor
|
||||
onClicked: {
|
||||
PanelService.getPanel("controlCenterPanel")?.toggle(this)
|
||||
}
|
||||
onRightClicked: {
|
||||
if (action.running) {
|
||||
action.signal(15);
|
||||
@@ -164,37 +167,49 @@ Scope {
|
||||
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 {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 10
|
||||
}
|
||||
|
||||
Ip {
|
||||
showCountryCode: true
|
||||
}
|
||||
|
||||
CpuTemp {
|
||||
}
|
||||
|
||||
MemUsage {
|
||||
}
|
||||
|
||||
CpuUsage {
|
||||
}
|
||||
|
||||
Battery {
|
||||
}
|
||||
|
||||
Brightness {
|
||||
screen: modelData
|
||||
}
|
||||
|
||||
Volume {
|
||||
LyricsBar {
|
||||
id: lyricsBar
|
||||
visible: SettingsService.showLyricsBar
|
||||
width: 600
|
||||
}
|
||||
|
||||
Item {
|
||||
|
||||
@@ -2,8 +2,8 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Constants
|
||||
import qs.Modules.Misc
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
@@ -78,7 +78,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.pointSize: Fonts.medium
|
||||
font.family: Fonts.primary
|
||||
color: Colors.accent
|
||||
color: Colors.primary
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
@@ -29,7 +29,7 @@ Item {
|
||||
|
||||
Text {
|
||||
text: Icons.global
|
||||
font.pointSize: Fonts.icon + 5
|
||||
font.pointSize: Fonts.icon + 6
|
||||
color: Colors.peach
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ Item {
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
duration: Style.animationFast
|
||||
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: Icons.download
|
||||
font.pointSize: Fonts.icon - 3
|
||||
color: Colors.accent
|
||||
color: Colors.primary
|
||||
Layout.leftMargin: 10
|
||||
}
|
||||
|
||||
Text {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||
font.pointSize: Fonts.medium
|
||||
font.family: Fonts.primary
|
||||
color: Colors.accent
|
||||
color: Colors.primary
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -37,14 +37,14 @@ Item {
|
||||
Text {
|
||||
text: Icons.upload
|
||||
font.pointSize: Fonts.icon - 3
|
||||
color: Colors.accent
|
||||
color: Colors.primary
|
||||
}
|
||||
|
||||
Text {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||
font.pointSize: Fonts.medium
|
||||
font.family: Fonts.primary
|
||||
color: Colors.accent
|
||||
color: Colors.primary
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,5 +6,13 @@ Text {
|
||||
text: TimeService.time + " | " + TimeService.dateString
|
||||
font.pointSize: Fonts.medium
|
||||
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 real masterProgress: 0
|
||||
property bool effectsActive: false
|
||||
property color effectColor: Colors.accent
|
||||
property color effectColor: Colors.primary
|
||||
property int horizontalPadding: 16
|
||||
property int spacingBetweenPills: 8
|
||||
property bool isDestroying: false
|
||||
|
||||
signal workspaceChanged(int workspaceId, color accentColor)
|
||||
signal workspaceChanged(int workspaceId, color primaryColor)
|
||||
|
||||
function triggerUnifiedWave() {
|
||||
effectColor = Colors.accent;
|
||||
effectColor = Colors.primary;
|
||||
masterAnimation.restart();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ Item {
|
||||
const ws = localWorkspaces.get(i);
|
||||
if (ws.isFocused === true) {
|
||||
root.triggerUnifiedWave();
|
||||
root.workspaceChanged(ws.id, Colors.accent);
|
||||
root.workspaceChanged(ws.id, Colors.primary);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -180,10 +180,10 @@ Item {
|
||||
}
|
||||
color: {
|
||||
if (model.isFocused)
|
||||
return Colors.accent;
|
||||
return Colors.primary;
|
||||
|
||||
if (model.isActive)
|
||||
return Colors.accent.lighter(130);
|
||||
return Colors.primary.lighter(130);
|
||||
|
||||
if (model.isUrgent)
|
||||
return Theme.error;
|
||||
|
||||
@@ -11,7 +11,7 @@ Item {
|
||||
property real maxValue: 100
|
||||
property real value: 100
|
||||
property string textValue: "" // override value in textDisplay if set
|
||||
property color fillColor: Colors.accent
|
||||
property color fillColor: Colors.primary
|
||||
property string textSuffix: ""
|
||||
property bool pointerCursor: true
|
||||
property alias hovered: mouseArea.containsMouse
|
||||
@@ -127,7 +127,7 @@ Item {
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ Item {
|
||||
required property string symbol
|
||||
property color buttonColor: Colors.distroColor
|
||||
readonly property alias hovered: mouseArea.containsMouse
|
||||
property real iconSize: Fonts.icon
|
||||
property real radius: Style.radiusS
|
||||
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
@@ -35,7 +37,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
text: symbol
|
||||
font.family: Fonts.nerd
|
||||
font.pointSize: Fonts.icon
|
||||
font.pointSize: iconSize
|
||||
font.bold: false
|
||||
color: buttonColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
@@ -46,11 +48,12 @@ Item {
|
||||
anchors.fill: parent
|
||||
color: parent.hovered ? buttonColor : Colors.transparent
|
||||
opacity: 0.3
|
||||
radius: 14
|
||||
radius: root.radius
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 120
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Quickshell.Widgets
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@@ -107,8 +108,7 @@ Rectangle {
|
||||
trayMenu.item.menu = modelData.menu
|
||||
trayMenu.item.showAt(parent, menuX, menuY)
|
||||
} else {
|
||||
// Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
|
||||
console.log("No menu available for", modelData.id, "or trayMenu not set")
|
||||
Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
import qs.Utils
|
||||
import qs.Noctalia
|
||||
|
||||
PopupWindow {
|
||||
id: root
|
||||
@@ -19,7 +21,7 @@ PopupWindow {
|
||||
implicitWidth: menuWidth
|
||||
|
||||
// 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
|
||||
color: Colors.transparent
|
||||
anchor.item: anchorItem
|
||||
@@ -28,7 +30,7 @@ PopupWindow {
|
||||
|
||||
function showAt(item, x, y) {
|
||||
if (!item) {
|
||||
console.warn("AnchorItem is undefined, won't show menu.")
|
||||
Logger.warn("TrayMenu", "AnchorItem is undefined, won't show menu.");
|
||||
return
|
||||
}
|
||||
|
||||
@@ -84,15 +86,15 @@ PopupWindow {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Colors.base
|
||||
border.color: Colors.accent
|
||||
border.color: Colors.primary
|
||||
border.width: 2
|
||||
radius: 14
|
||||
radius: Style.radiusM
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
anchors.margins: Style.marginS
|
||||
contentHeight: columnLayout.implicitHeight
|
||||
interactive: true
|
||||
|
||||
@@ -115,88 +117,56 @@ PopupWindow {
|
||||
return 8
|
||||
} else {
|
||||
// Calculate based on text content
|
||||
const textHeight = text.contentHeight || (Fonts.small)
|
||||
return textHeight + 16
|
||||
const textHeight = text.contentHeight || (Style.fontSizeS * 1.2)
|
||||
return Math.max(28, textHeight + (Style.marginS * 2))
|
||||
}
|
||||
}
|
||||
|
||||
color: Colors.transparent
|
||||
property var subMenu: null
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 16
|
||||
height: 1
|
||||
NDivider {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - (Style.marginM * 2)
|
||||
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 {
|
||||
anchors.fill: parent
|
||||
color: mouseArea.containsMouse ? Colors.accent : Colors.transparent
|
||||
radius: 10
|
||||
color: mouseArea.containsMouse ? Colors.primary : Colors.transparent
|
||||
radius: Style.radiusS
|
||||
visible: !(modelData?.isSeparator ?? false)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
anchors.leftMargin: Style.marginM
|
||||
anchors.rightMargin: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
Text {
|
||||
NText {
|
||||
id: text
|
||||
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, ' ') : "..."
|
||||
font.pointSize: Fonts.small
|
||||
pointSize: Style.fontSizeS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 14
|
||||
Layout.preferredHeight: 14
|
||||
Layout.preferredWidth: Style.marginL
|
||||
Layout.preferredHeight: Style.marginL
|
||||
source: modelData?.icon ?? ""
|
||||
visible: (modelData?.icon ?? "") !== ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
Text {
|
||||
NIcon {
|
||||
icon: modelData?.hasChildren ? "menu" : ""
|
||||
pointSize: Style.fontSizeS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: modelData?.hasChildren ?? false
|
||||
color: (mouseArea.containsMouse ? Colors.base : Colors.text)
|
||||
|
||||
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
|
||||
color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Utils
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -60,6 +61,7 @@ Singleton {
|
||||
|
||||
function onMutedChanged() {
|
||||
root._muted = (sink?.audio.muted ?? true)
|
||||
Logger.log("AudioService", "OnMuteChanged:", root._muted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +78,7 @@ Singleton {
|
||||
|
||||
function onMutedChanged() {
|
||||
root._inputMuted = (source?.audio.muted ?? true)
|
||||
Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ Singleton {
|
||||
sink.audio.muted = false
|
||||
sink.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
||||
} else {
|
||||
console.warn("No sink available")
|
||||
Logger.warn("AudioService", "No sink available")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +104,7 @@ Singleton {
|
||||
if (sink?.ready && sink?.audio) {
|
||||
sink.audio.muted = muted
|
||||
} else {
|
||||
console.warn("No sink available")
|
||||
Logger.warn("AudioService", "No sink available")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +114,7 @@ Singleton {
|
||||
source.audio.muted = false
|
||||
source.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
||||
} else {
|
||||
console.warn("No source available")
|
||||
Logger.warn("AudioService", "No source available")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +122,7 @@ Singleton {
|
||||
if (source?.ready && source?.audio) {
|
||||
source.audio.muted = muted
|
||||
} else {
|
||||
console.warn("No source available")
|
||||
Logger.warn("AudioService", "No source available")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Utils
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -44,6 +45,10 @@ Singleton {
|
||||
|
||||
reloadableId: "brightness"
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("Brightness", "Service started")
|
||||
}
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = []
|
||||
ddcProc.running = true
|
||||
@@ -80,7 +85,7 @@ Singleton {
|
||||
var ddcModel = ddcModelMatch ? ddcModelMatch.length > 0 : false
|
||||
var model = modelMatch ? modelMatch[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 {
|
||||
"model": model,
|
||||
"busNum": bus,
|
||||
@@ -188,7 +193,7 @@ Singleton {
|
||||
var val = parseInt(dataText)
|
||||
if (!isNaN(val)) {
|
||||
monitor.brightness = val / 101
|
||||
console.log("Apple display brightness:", monitor.brightness)
|
||||
Logger.log("Brightness", "Apple display brightness:", monitor.brightness)
|
||||
}
|
||||
} else if (monitor.isDdc) {
|
||||
var parts = dataText.split(" ")
|
||||
@@ -197,7 +202,7 @@ Singleton {
|
||||
var max = parseInt(parts[4])
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.brightness = current / max
|
||||
console.log("DDC brightness:", monitor.brightness)
|
||||
Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -213,8 +218,8 @@ Singleton {
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.maxBrightness = max
|
||||
monitor.brightness = current / max
|
||||
console.log("Internal brightness:", monitor.brightness)
|
||||
console.log("Using backlight device:", monitor.backlightDevice)
|
||||
Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
|
||||
Logger.log("Brightness", "Using backlight device:", monitor.backlightDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
@@ -136,10 +137,10 @@ Singleton {
|
||||
if (isInhibited)
|
||||
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() {
|
||||
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 Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
@@ -9,28 +10,93 @@ Singleton {
|
||||
property real fetchInterval: 30 // in s
|
||||
property real fetchTimeout: 10 // in s
|
||||
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: ""
|
||||
|
||||
function fetchIP() {
|
||||
if (fetchIPProcess.running) {
|
||||
console.warn("Fetch IP process is still running, skipping fetchIP");
|
||||
return ;
|
||||
}
|
||||
fetchIPProcess.running = true;
|
||||
const xhr = new XMLHttpRequest();
|
||||
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.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() {
|
||||
if (fetchGeoProcess.running) {
|
||||
console.warn("Fetch geo process is still running, skipping fetchGeoInfo");
|
||||
return ;
|
||||
}
|
||||
if (!ip || ip === "N/A") {
|
||||
countryCode = "N/A";
|
||||
return ;
|
||||
}
|
||||
fetchGeoProcess.command = ["sh", "-c", `curl -L -m ${fetchTimeout.toString()} ${geoURL}${ip}${geoURLToken ? "?token=" + geoURLToken : ""}`];
|
||||
fetchGeoProcess.running = true;
|
||||
const xhr = new XMLHttpRequest();
|
||||
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() {
|
||||
@@ -46,11 +112,11 @@ Singleton {
|
||||
FileView {
|
||||
id: tokenFile
|
||||
|
||||
path: Qt.resolvedUrl("../Assets/Ip/token.txt")
|
||||
path: Qt.resolvedUrl("../Assets/Config/GeoInfoToken.txt")
|
||||
onLoaded: {
|
||||
geoURLToken = tokenFile.text();
|
||||
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();
|
||||
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.Services.Mpris
|
||||
import qs.Modules.Misc
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
@@ -11,6 +12,7 @@ Singleton {
|
||||
property var currentPlayer: null
|
||||
property real currentPosition: 0
|
||||
property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false
|
||||
property int selectedPlayerIndex: -1
|
||||
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
|
||||
property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
|
||||
property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
|
||||
@@ -59,11 +61,25 @@ Singleton {
|
||||
|
||||
// Updates currentPlayer and currentPosition
|
||||
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();
|
||||
if (newPlayer !== currentPlayer) {
|
||||
currentPlayer = newPlayer;
|
||||
currentPosition = currentPlayer ? currentPlayer.position : 0;
|
||||
}
|
||||
Logger.log("MusicManager", "Current player updated:", currentPlayer ? currentPlayer.identity : "None");
|
||||
}
|
||||
|
||||
// Player control functions
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
@@ -66,7 +67,7 @@ Singleton {
|
||||
});
|
||||
root.workspaces = workspacesList;
|
||||
} 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;
|
||||
} catch (e) {
|
||||
console.error("Error parsing windows event:", e);
|
||||
Logger.error("Niri", "Error parsing windows event:", e);
|
||||
}
|
||||
} else if (event.WorkspaceActivated) {
|
||||
workspaceProcess.running = true;
|
||||
@@ -120,13 +121,13 @@ Singleton {
|
||||
root.focusedWindowId = -1;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing window focus event:", e);
|
||||
Logger.error("Niri", "Error parsing window focus event:", e);
|
||||
}
|
||||
} else if (event.OverviewOpenedOrClosed) {
|
||||
try {
|
||||
root.inOverview = event.OverviewOpenedOrClosed.is_open === true;
|
||||
} catch (e) {
|
||||
console.error("Error parsing overview state:", e);
|
||||
Logger.error("Niri", "Error parsing overview state:", e);
|
||||
}
|
||||
} else if (event.WindowOpenedOrChanged) {
|
||||
try {
|
||||
@@ -161,7 +162,7 @@ Singleton {
|
||||
|
||||
}
|
||||
} 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) {
|
||||
try {
|
||||
@@ -170,11 +171,11 @@ Singleton {
|
||||
delete root.windows[closedId];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing window closed event:", e);
|
||||
Logger.error("Niri", "Error parsing window closed event:", 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 {
|
||||
id: root
|
||||
|
||||
function show(title, message, icon = "", urgency = "normal", timeout = 5000) {
|
||||
function show(title, message, timeout = 5000, icon = "", urgency = "normal") {
|
||||
if (icon)
|
||||
action.command = ["notify-send", "-u", urgency, "-i", icon, "-t", timeout.toString(), title, message];
|
||||
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 Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
@@ -222,7 +223,7 @@ Singleton {
|
||||
}
|
||||
root.cpuTemp = Math.round(sum / root.intelTempValues.length);
|
||||
} else {
|
||||
console.warn("No temperature sensors found for coretemp");
|
||||
Logger.warn("SystemStatService", "No temperature sensors found for coretemp");
|
||||
root.cpuTemp = 0;
|
||||
}
|
||||
return ;
|
||||
@@ -328,7 +329,7 @@ Singleton {
|
||||
function checkNext() {
|
||||
if (currentIndex >= 16) {
|
||||
// Check up to hwmon10
|
||||
console.warn("No supported temperature sensor found");
|
||||
Logger.warn("SystemStatService", "No supported temperature sensor found");
|
||||
return ;
|
||||
}
|
||||
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`;
|
||||
@@ -341,7 +342,7 @@ Singleton {
|
||||
if (root.supportedTempCpuSensorNames.includes(name)) {
|
||||
root.cpuTempSensorName = name;
|
||||
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 {
|
||||
currentIndex++;
|
||||
Qt.callLater(() => {
|
||||
@@ -370,7 +371,6 @@ Singleton {
|
||||
if (root.cpuTempSensorName === "coretemp") {
|
||||
// For Intel, collect all temperature values
|
||||
const temp = parseInt(data) / 1000;
|
||||
//console.log(temp, cpuTempReader.path)
|
||||
root.intelTempValues.push(temp);
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
|
||||
@@ -5,6 +5,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -40,7 +41,7 @@ Singleton {
|
||||
try {
|
||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]);
|
||||
} 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": {
|
||||
"method": "raw",
|
||||
"bit_format": 8,
|
||||
"data_format": "ascii",
|
||||
"ascii_max_range": 100,
|
||||
"bit_format": "8bit",
|
||||
"channels": channels,
|
||||
"mono_option": monoOption
|
||||
}
|
||||
@@ -33,14 +35,11 @@ Scope {
|
||||
Process {
|
||||
id: process
|
||||
|
||||
property int index: 0
|
||||
|
||||
stdinEnabled: true
|
||||
running: !MusicManager.isAllPaused()
|
||||
command: ["cava", "-p", "/dev/stdin"]
|
||||
onExited: {
|
||||
stdinEnabled = true;
|
||||
index = 0;
|
||||
values = Array(count).fill(0);
|
||||
}
|
||||
onStarted: {
|
||||
@@ -56,23 +55,14 @@ Scope {
|
||||
}
|
||||
}
|
||||
stdinEnabled = false;
|
||||
values = Array(count).fill(0);
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: (data) => {
|
||||
const newValues = Array(count).fill(0);
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
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;
|
||||
root.values = data.slice(0, -1).split(";").map((v) => {
|
||||
return parseInt(v, 10) / 100;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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.Modules.Bar
|
||||
import qs.Modules.Misc
|
||||
import qs.Modules.Panel
|
||||
import qs.Services
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
IPCService {
|
||||
id: ipcService
|
||||
}
|
||||
|
||||
Bar {
|
||||
id: bar
|
||||
|
||||
@@ -20,4 +26,16 @@ Scope {
|
||||
shell: root
|
||||
}
|
||||
|
||||
CalendarPanel {
|
||||
id: calendarPanel
|
||||
|
||||
objectName: "calendarPanel"
|
||||
}
|
||||
|
||||
ControlCenterPanel {
|
||||
id: controlCenterPanel
|
||||
|
||||
objectName: "controlCenterPanel"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
peach: #fab387;
|
||||
search: rgba(49, 50, 68, 0.5); // alpha(@surface, 0.5)
|
||||
|
||||
accent: #89b4fa;
|
||||
primary: #89b4fa;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ window {
|
||||
padding: 0px;
|
||||
border: 2px solid;
|
||||
border-radius: 24px;
|
||||
border-color: @accent;
|
||||
border-color: @primary;
|
||||
cursor: "default";
|
||||
background-color: @base;
|
||||
}
|
||||
@@ -126,7 +126,7 @@ listview {
|
||||
}
|
||||
scrollbar {
|
||||
handle-width: 5px ;
|
||||
handle-color: @accent;
|
||||
handle-color: @primary;
|
||||
border-radius: 10px;
|
||||
background-color: @mantle;
|
||||
}
|
||||
@@ -150,7 +150,7 @@ element alternate.normal {
|
||||
element normal.urgent,
|
||||
element alternate.urgent,
|
||||
element selected.active {
|
||||
background-color: @accent;
|
||||
background-color: @primary;
|
||||
text-color: @base;
|
||||
border-radius: 10px;
|
||||
}
|
||||
@@ -198,9 +198,9 @@ button {
|
||||
}
|
||||
button selected {
|
||||
background-color: @surface;
|
||||
text-color: @accent;
|
||||
text-color: @primary;
|
||||
border-radius: 0 0 14px 14px;
|
||||
border-color: @accent;
|
||||
border-color: @primary;
|
||||
}
|
||||
|
||||
/*****----- Message -----*****/
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
peach: #fab387;
|
||||
search: rgba(49, 50, 68, 0.5); // alpha(@surface, 0.5)
|
||||
|
||||
accent: #<FLAVOR_HEX>;
|
||||
primary: #<FLAVOR_HEX>;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ window {
|
||||
padding: 0px;
|
||||
border: 2px solid;
|
||||
border-radius: 24px;
|
||||
border-color: @accent;
|
||||
border-color: @primary;
|
||||
cursor: "default";
|
||||
background-color: @base;
|
||||
}
|
||||
@@ -126,7 +126,7 @@ listview {
|
||||
}
|
||||
scrollbar {
|
||||
handle-width: 5px ;
|
||||
handle-color: @accent;
|
||||
handle-color: @primary;
|
||||
border-radius: 10px;
|
||||
background-color: @mantle;
|
||||
}
|
||||
@@ -150,7 +150,7 @@ element alternate.normal {
|
||||
element normal.urgent,
|
||||
element alternate.urgent,
|
||||
element selected.active {
|
||||
background-color: @accent;
|
||||
background-color: @primary;
|
||||
text-color: @base;
|
||||
border-radius: 10px;
|
||||
}
|
||||
@@ -198,9 +198,9 @@ button {
|
||||
}
|
||||
button selected {
|
||||
background-color: @surface;
|
||||
text-color: @accent;
|
||||
text-color: @primary;
|
||||
border-radius: 0 0 14px 14px;
|
||||
border-color: @accent;
|
||||
border-color: @primary;
|
||||
}
|
||||
|
||||
/*****----- 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