quickshell: notification daemon
This commit is contained in:
@@ -415,7 +415,7 @@ def main():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error while tweaking {app}: {e}")
|
print(f"Error while tweaking {app}: {e}")
|
||||||
|
|
||||||
os.system(f'notify-send "Color theme changed" "Palette: {palette_name}\nFlavor: {flavor}"')
|
os.system(f'notify-send -a "change-colortheme" "Color theme changed" "Palette: {palette_name}\nFlavor: {flavor}"')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ blurred_image="$blur_dir/blurred-${random_name}.$ext"
|
|||||||
|
|
||||||
## Time consuming task (magick -blur) in background
|
## Time consuming task (magick -blur) in background
|
||||||
(
|
(
|
||||||
# notify-send "Generating Blurred Wallpaper" "This may take a few seconds..."
|
# notify-send -a "change-wallpaper" "Generating Blurred Wallpaper" "This may take a few seconds..."
|
||||||
|
|
||||||
sigma=$(magick identify -format "%w %h" "$image_copied" | awk -v f=0.01 '{
|
sigma=$(magick identify -format "%w %h" "$image_copied" | awk -v f=0.01 '{
|
||||||
m=($1>$2)?$1:$2;
|
m=($1>$2)?$1:$2;
|
||||||
@@ -70,7 +70,7 @@ blurred_image="$blur_dir/blurred-${random_name}.$ext"
|
|||||||
swww img -n backdrop "$blurred_image" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
|
swww img -n backdrop "$blurred_image" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
notify-send "Blurred Wallpaper Generated" "$blurred_image"
|
notify-send -a "change-wallpaper" "Blurred Wallpaper Generated" "$blurred_image" -i "$blurred_image"
|
||||||
) &
|
) &
|
||||||
|
|
||||||
# Apply wallpaper
|
# Apply wallpaper
|
||||||
@@ -78,13 +78,13 @@ blurred_image="$blur_dir/blurred-${random_name}.$ext"
|
|||||||
if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
|
if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
|
||||||
swww img -n background "$image_copied" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
|
swww img -n background "$image_copied" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
|
||||||
|
|
||||||
notify-send "Wallpaper Changed" "$image"
|
notify-send -a "change-wallpaper" "Wallpaper Changed" "$image" -i "$image_copied"
|
||||||
|
|
||||||
change-colortheme -i "$image_copied" !quickshell || exit 1
|
change-colortheme -i "$image_copied" !quickshell || exit 1
|
||||||
elif [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
|
elif [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
|
||||||
swww img -n background "$image_copied" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
|
swww img -n background "$image_copied" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
|
||||||
|
|
||||||
notify-send "Wallpaper Changed" "$image"
|
notify-send -a "change-wallpaper" "Wallpaper Changed" "$image" -i "$image_copied"
|
||||||
|
|
||||||
change-colortheme -i "$image_copied" !waybar !eww || exit 1
|
change-colortheme -i "$image_copied" !waybar !eww || exit 1
|
||||||
else
|
else
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -15,24 +15,24 @@
|
|||||||
|
|
||||||
## Setup Overview
|
## Setup Overview
|
||||||
|
|
||||||
- **OS**: Archlinux
|
- OS: Archlinux
|
||||||
- **WM**: Hyprland | **Niri**
|
- WM: Hyprland | **Niri**
|
||||||
- **Bar**: Waybar | **Quickshell**
|
- Bar: Waybar | **Quickshell**
|
||||||
- **Shell**: Fish
|
- Shell: Fish
|
||||||
- **Prompt**: Oh My Posh
|
- Prompt: Oh My Posh
|
||||||
- **Terminal**: **Kitty** & Ghostty
|
- Terminal: **Kitty** & Ghostty
|
||||||
- **Colorscheme**: Catppuccin Mocha
|
- Colorscheme: Catppuccin Mocha
|
||||||
- **App Launcher**: Rofi
|
- App Launcher: Rofi
|
||||||
- **Logout Screen**: Wlogout
|
- Logout Screen: Wlogout
|
||||||
- **Desktop Widgets**: Eww | **Quickshell**
|
- Desktop Widgets: Eww | **Quickshell**
|
||||||
- **Wallpaper Daemon**: Swww
|
- Wallpaper Daemon: Swww
|
||||||
- **Notification Daemon**: Mako
|
- Notification Daemon: Mako | **Quickshell**
|
||||||
|
|
||||||
(**bold**: I currently prefer)
|
(**bold**: I currently prefer)
|
||||||
|
|
||||||
## Hyprland & friends
|
## Hyprland & friends
|
||||||
|
|
||||||
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.
|
Based on an old version of [end-4/dots-hyprland](https://github.com/end-4/dots-hyprland) but without ags, quickshell, eww and tons of other stuff.
|
||||||
|
|
||||||
## Niri
|
## Niri
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ spawn-at-startup "blueman-applet"
|
|||||||
spawn-at-startup "nm-applet"
|
spawn-at-startup "nm-applet"
|
||||||
spawn-sh-at-startup "gnome-keyring-daemon --start --components=secrets"
|
spawn-sh-at-startup "gnome-keyring-daemon --start --components=secrets"
|
||||||
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
|
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
|
||||||
spawn-at-startup "mako"
|
|
||||||
|
|
||||||
// Idle
|
// Idle
|
||||||
spawn-sh-at-startup "hypridle"
|
spawn-sh-at-startup "hypridle"
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ spawn-at-startup "blueman-applet"
|
|||||||
spawn-at-startup "nm-applet"
|
spawn-at-startup "nm-applet"
|
||||||
spawn-sh-at-startup "gnome-keyring-daemon --start --components=secrets"
|
spawn-sh-at-startup "gnome-keyring-daemon --start --components=secrets"
|
||||||
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
|
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
|
||||||
spawn-at-startup "mako"
|
|
||||||
|
|
||||||
// Idle
|
// Idle
|
||||||
spawn-sh-at-startup "hypridle"
|
spawn-sh-at-startup "hypridle"
|
||||||
|
|||||||
3
quickshell/Assets/Config/.gitignore
vendored
3
quickshell/Assets/Config/.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
# some sensitive files
|
# some sensitive files
|
||||||
Location.json
|
Location.json
|
||||||
GeoInfoToken.txt
|
GeoInfoToken.txt
|
||||||
IpCache.json
|
IpCache.json
|
||||||
|
Notifications.json
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"notifications": {
|
||||||
|
"doNotDisturb": false,
|
||||||
|
"lastSeenTs": 1760375164000
|
||||||
|
},
|
||||||
"primaryColor": "#89b4fa",
|
"primaryColor": "#89b4fa",
|
||||||
"showLyricsBar": false
|
"showLyricsBar": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Singleton {
|
|||||||
property color mSurface: Colors.base
|
property color mSurface: Colors.base
|
||||||
property color mOnSurface: Colors.text
|
property color mOnSurface: Colors.text
|
||||||
property color mSurfaceVariant: Colors.surface
|
property color mSurfaceVariant: Colors.surface
|
||||||
property color mOnSurfaceVariant: Colors.text
|
property color mOnSurfaceVariant: Colors.overlay1
|
||||||
property color mOutline: Colors.primary
|
property color mOutline: Colors.primary
|
||||||
property color mShadow: Colors.crust
|
property color mShadow: Colors.crust
|
||||||
property color transparent: "transparent"
|
property color transparent: "transparent"
|
||||||
|
|||||||
@@ -11,255 +11,241 @@ import qs.Modules.Bar.Misc
|
|||||||
import qs.Modules.Misc
|
import qs.Modules.Misc
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
Scope {
|
Variants {
|
||||||
id: rootScope
|
model: Quickshell.screens
|
||||||
|
|
||||||
property var shell
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: barRootItem
|
property var modelData
|
||||||
|
|
||||||
anchors.fill: parent
|
PanelWindow {
|
||||||
|
id: panel
|
||||||
|
|
||||||
Variants {
|
screen: modelData
|
||||||
model: Quickshell.screens
|
WlrLayershell.namespace: "quickshell-bar"
|
||||||
|
color: Colors.transparent
|
||||||
|
implicitHeight: Style.barHeight
|
||||||
|
|
||||||
Item {
|
anchors {
|
||||||
property var modelData
|
left: true
|
||||||
|
right: true
|
||||||
|
top: true
|
||||||
|
}
|
||||||
|
|
||||||
PanelWindow {
|
Rectangle {
|
||||||
id: panel
|
id: barBackground
|
||||||
|
|
||||||
screen: modelData
|
anchors.fill: parent
|
||||||
WlrLayershell.namespace: "quickshell-bar"
|
color: Niri.noFocus ? null : Colors.base
|
||||||
color: Colors.transparent
|
|
||||||
implicitHeight: Style.barHeight
|
|
||||||
|
|
||||||
anchors {
|
gradient: Gradient {
|
||||||
left: true
|
GradientStop {
|
||||||
right: true
|
position: 0
|
||||||
top: true
|
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0.8 : 1)
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: barBackground
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Niri.noFocus ? null : Colors.base
|
|
||||||
|
|
||||||
gradient: Gradient {
|
|
||||||
GradientStop {
|
|
||||||
position: 0
|
|
||||||
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0.8 : 1)
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Style.animationSlowest
|
|
||||||
easing.type: Easing.InOutCubic
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
GradientStop {
|
|
||||||
position: 1
|
|
||||||
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0 : 1)
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Style.animationSlowest
|
|
||||||
easing.type: Easing.InOutCubic
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationSlowest
|
||||||
|
easing.type: Easing.InOutCubic
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
GradientStop {
|
||||||
id: leftLayout
|
position: 1
|
||||||
|
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0 : 1)
|
||||||
|
|
||||||
height: parent.height - 10
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
anchors {
|
duration: Style.animationSlowest
|
||||||
left: parent.left
|
easing.type: Easing.InOutCubic
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
leftMargin: 5
|
|
||||||
}
|
|
||||||
|
|
||||||
SymbolButton {
|
|
||||||
symbol: Icons.distro
|
|
||||||
buttonColor: Colors.distroColor
|
|
||||||
onClicked: {
|
|
||||||
PanelService.getPanel("controlCenterPanel")?.toggle(this)
|
|
||||||
}
|
|
||||||
onRightClicked: {
|
|
||||||
if (action.running) {
|
|
||||||
action.signal(15);
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
action.exec(["rofi", "-show", "drun"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Separator {
|
|
||||||
}
|
|
||||||
|
|
||||||
Workspace {
|
|
||||||
screen: modelData
|
|
||||||
}
|
|
||||||
|
|
||||||
Separator {
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 10
|
|
||||||
}
|
|
||||||
|
|
||||||
CavaBar {
|
|
||||||
count: 6
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 10
|
|
||||||
}
|
|
||||||
|
|
||||||
Separator {
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 10
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusedWindow {
|
|
||||||
maxWidth: 400
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: middleLayout
|
|
||||||
|
|
||||||
height: parent.height - 10
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
horizontalCenter: parent.horizontalCenter
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Time {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: rightLayout
|
|
||||||
|
|
||||||
height: parent.height - 10
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
right: parent.right
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
rightMargin: 5
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LyricsBar {
|
|
||||||
id: lyricsBar
|
|
||||||
visible: SettingsService.showLyricsBar
|
|
||||||
width: 600
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 5
|
|
||||||
}
|
|
||||||
|
|
||||||
Separator {
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 5
|
|
||||||
}
|
|
||||||
|
|
||||||
TrayExpander {
|
|
||||||
screen: modelData
|
|
||||||
}
|
|
||||||
|
|
||||||
SymbolButton {
|
|
||||||
symbol: Caffeine.isInhibited ? Icons.idleInhibitorActivated : Icons.idleInhibitorDeactivated
|
|
||||||
buttonColor: Caffeine.isInhibited ? Colors.peach : Colors.yellow
|
|
||||||
onClicked: {
|
|
||||||
Caffeine.manualToggle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SymbolButton {
|
|
||||||
symbol: Icons.powerMenu
|
|
||||||
buttonColor: Colors.red
|
|
||||||
onClicked: {
|
|
||||||
if (action.running) {
|
|
||||||
action.signal(15);
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
action.exec(["wlogout"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: action
|
|
||||||
|
|
||||||
running: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: leftLayout
|
||||||
|
|
||||||
|
height: parent.height - 10
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
leftMargin: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
SymbolButton {
|
||||||
|
symbol: Icons.distro
|
||||||
|
buttonColor: Colors.distroColor
|
||||||
|
onClicked: {
|
||||||
|
PanelService.getPanel("controlCenterPanel")?.toggle(this)
|
||||||
|
}
|
||||||
|
onRightClicked: {
|
||||||
|
if (action.running) {
|
||||||
|
action.signal(15);
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
action.exec(["rofi", "-show", "drun"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Separator {
|
||||||
|
}
|
||||||
|
|
||||||
|
Workspace {
|
||||||
|
screen: modelData
|
||||||
|
}
|
||||||
|
|
||||||
|
Separator {
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
CavaBar {
|
||||||
|
count: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
Separator {
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusedWindow {
|
||||||
|
maxWidth: 400
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: middleLayout
|
||||||
|
|
||||||
|
height: parent.height - 10
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Time {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: rightLayout
|
||||||
|
|
||||||
|
height: parent.height - 10
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
right: parent.right
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
rightMargin: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LyricsBar {
|
||||||
|
id: lyricsBar
|
||||||
|
visible: SettingsService.showLyricsBar
|
||||||
|
width: 600
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
Separator {
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
TrayExpander {
|
||||||
|
screen: modelData
|
||||||
|
}
|
||||||
|
|
||||||
|
SymbolButton {
|
||||||
|
symbol: Caffeine.isInhibited ? Icons.idleInhibitorActivated : Icons.idleInhibitorDeactivated
|
||||||
|
buttonColor: Caffeine.isInhibited ? Colors.peach : Colors.yellow
|
||||||
|
onClicked: {
|
||||||
|
Caffeine.manualToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SymbolButton {
|
||||||
|
symbol: Icons.powerMenu
|
||||||
|
buttonColor: Colors.red
|
||||||
|
onClicked: {
|
||||||
|
if (action.running) {
|
||||||
|
action.signal(15);
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
Quickshell.execDetached(["wlogout"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: action
|
||||||
|
|
||||||
|
running: false
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell.Io
|
import Quickshell
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import qs.Constants
|
import qs.Constants
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -80,12 +80,6 @@ Item {
|
|||||||
font.family: Fonts.primary
|
font.family: Fonts.primary
|
||||||
color: Colors.primary
|
color: Colors.primary
|
||||||
|
|
||||||
Process {
|
|
||||||
id: action
|
|
||||||
|
|
||||||
running: false
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
|
|
||||||
@@ -102,28 +96,20 @@ Item {
|
|||||||
windowTitle.x = 0;
|
windowTitle.x = 0;
|
||||||
}
|
}
|
||||||
onClicked: function(mouse) {
|
onClicked: function(mouse) {
|
||||||
if (mouse.button === Qt.MiddleButton) {
|
if (mouse.button === Qt.MiddleButton)
|
||||||
action.command = ["niri", "msg", "action", "close-window"];
|
Quickshell.execDetached(["niri", "msg", "action", "close-window"]);
|
||||||
action.startDetached();
|
else if (mouse.button === Qt.LeftButton)
|
||||||
} else if (mouse.button === Qt.LeftButton) {
|
Quickshell.execDetached(["niri", "msg", "action", "center-window"]);
|
||||||
action.command = ["niri", "msg", "action", "center-window"];
|
|
||||||
action.startDetached();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onWheel: function(wheel) {
|
onWheel: function(wheel) {
|
||||||
if (wheel.angleDelta.y > 0) {
|
if (wheel.angleDelta.y > 0)
|
||||||
action.command = ["niri", "msg", "action", "set-column-width", "+10%"];
|
Quickshell.execDetached(["niri", "msg", "action", "set-column-width", "+10%"]);
|
||||||
action.startDetached();
|
else if (wheel.angleDelta.y < 0)
|
||||||
} else if (wheel.angleDelta.y < 0) {
|
Quickshell.execDetached(["niri", "msg", "action", "set-column-width", "-10%"]);
|
||||||
action.command = ["niri", "msg", "action", "set-column-width", "-10%"];
|
else if (wheel.angleDelta.x > 0)
|
||||||
action.startDetached();
|
Quickshell.execDetached(["niri", "msg", "action", "focus-column-left"]);
|
||||||
} else if (wheel.angleDelta.x > 0) {
|
else if (wheel.angleDelta.x < 0)
|
||||||
action.command = ["niri", "msg", "action", "focus-column-left"];
|
Quickshell.execDetached(["niri", "msg", "action", "focus-column-right"]);
|
||||||
action.startDetached();
|
|
||||||
} else if (wheel.angleDelta.x < 0) {
|
|
||||||
action.command = ["niri", "msg", "action", "focus-column-right"];
|
|
||||||
action.startDetached();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ Text {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
PanelService.getPanel("calendarPanel")?.toggle(this)
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.LeftButton)
|
||||||
|
PanelService.getPanel("calendarPanel")?.toggle(this)
|
||||||
|
else if (mouse.button === Qt.RightButton)
|
||||||
|
PanelService.getPanel("notificationHistoryPanel")?.toggle(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
439
quickshell/Modules/Misc/Notification.qml
Normal file
439
quickshell/Modules/Misc/Notification.qml
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
// Simple notification popup - displays multiple notifications
|
||||||
|
Variants {
|
||||||
|
// Force removal without animation as fallback
|
||||||
|
|
||||||
|
// If no notification display activated in settings, then show them all
|
||||||
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
delegate: Loader {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property ShellScreen modelData
|
||||||
|
property real scaling: 1
|
||||||
|
// Access the notification model from the service
|
||||||
|
property ListModel notificationModel: NotificationService.activeList
|
||||||
|
|
||||||
|
// Loader is active when there are notifications
|
||||||
|
active: notificationModel.count > 0 || delayTimer.running
|
||||||
|
|
||||||
|
// Keep loader active briefly after last notification to allow animations to complete
|
||||||
|
Timer {
|
||||||
|
id: delayTimer
|
||||||
|
|
||||||
|
interval: Style.animationSlow + 200 // Animation duration + buffer
|
||||||
|
repeat: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start delay timer when last notification is removed
|
||||||
|
Connections {
|
||||||
|
function onCountChanged() {
|
||||||
|
if (notificationModel.count === 0 && root.active)
|
||||||
|
delayTimer.restart();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
target: notificationModel
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent: PanelWindow {
|
||||||
|
readonly property string location: "top_right"
|
||||||
|
readonly property bool isTop: (location === "top") || (location.length >= 3 && location.substring(0, 3) === "top")
|
||||||
|
readonly property bool isBottom: (location === "bottom") || (location.length >= 6 && location.substring(0, 6) === "bottom")
|
||||||
|
readonly property bool isLeft: location.indexOf("_left") >= 0
|
||||||
|
readonly property bool isRight: location.indexOf("_right") >= 0
|
||||||
|
readonly property bool isCentered: (location === "top" || location === "bottom")
|
||||||
|
// Store connection for cleanup
|
||||||
|
property var animateConnection: null
|
||||||
|
|
||||||
|
screen: modelData
|
||||||
|
WlrLayershell.namespace: "noctalia-notifications"
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
color: Color.transparent
|
||||||
|
// Anchor selection based on location (window edges)
|
||||||
|
anchors.top: isTop
|
||||||
|
anchors.bottom: isBottom
|
||||||
|
anchors.left: isLeft
|
||||||
|
anchors.right: isRight
|
||||||
|
// Margins depending on bar position and chosen location
|
||||||
|
margins.top: Style.barHeight + Style.marginM
|
||||||
|
margins.bottom: 0
|
||||||
|
margins.left: 0
|
||||||
|
margins.right: Style.marginM
|
||||||
|
implicitWidth: 360
|
||||||
|
implicitHeight: notificationStack.implicitHeight
|
||||||
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
|
// Connect to animation signal from service
|
||||||
|
Component.onCompleted: {
|
||||||
|
animateConnection = NotificationService.animateAndRemove.connect(function(notificationId) {
|
||||||
|
// Find the delegate by notification ID
|
||||||
|
var delegate = null;
|
||||||
|
if (notificationStack && notificationStack.children && notificationStack.children.length > 0) {
|
||||||
|
for (var i = 0; i < notificationStack.children.length; i++) {
|
||||||
|
var child = notificationStack.children[i];
|
||||||
|
if (child && child.notificationId === notificationId) {
|
||||||
|
delegate = child;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (delegate && delegate.animateOut)
|
||||||
|
delegate.animateOut();
|
||||||
|
else
|
||||||
|
NotificationService.dismissActiveNotification(notificationId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Disconnect when destroyed to prevent memory leaks
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (animateConnection) {
|
||||||
|
NotificationService.animateAndRemove.disconnect(animateConnection);
|
||||||
|
animateConnection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main notification container
|
||||||
|
ColumnLayout {
|
||||||
|
id: notificationStack
|
||||||
|
|
||||||
|
// Anchor the stack inside the window based on chosen location
|
||||||
|
anchors.top: parent.isTop ? parent.top : undefined
|
||||||
|
anchors.bottom: parent.isBottom ? parent.bottom : undefined
|
||||||
|
anchors.left: parent.isLeft ? parent.left : undefined
|
||||||
|
anchors.right: parent.isRight ? parent.right : undefined
|
||||||
|
anchors.horizontalCenter: parent.isCentered ? parent.horizontalCenter : undefined
|
||||||
|
spacing: Style.marginS
|
||||||
|
width: 360
|
||||||
|
visible: true
|
||||||
|
|
||||||
|
// Multiple notifications display
|
||||||
|
Repeater {
|
||||||
|
model: notificationModel
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: card
|
||||||
|
|
||||||
|
// Store the notification ID and data for reference
|
||||||
|
property string notificationId: model.id
|
||||||
|
property var notificationData: model
|
||||||
|
// Animation properties
|
||||||
|
property real scaleValue: 0.8
|
||||||
|
property real opacityValue: 0
|
||||||
|
property bool isRemoving: false
|
||||||
|
|
||||||
|
// Animate out when being removed
|
||||||
|
function animateOut() {
|
||||||
|
if (isRemoving)
|
||||||
|
return ;
|
||||||
|
|
||||||
|
// Prevent multiple animations
|
||||||
|
isRemoving = true;
|
||||||
|
scaleValue = 0.8;
|
||||||
|
opacityValue = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.preferredWidth: 360
|
||||||
|
Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2)
|
||||||
|
Layout.maximumHeight: Layout.preferredHeight
|
||||||
|
radius: Style.radiusL
|
||||||
|
border.color: Colors.overlay0
|
||||||
|
border.width: Math.max(1, Style.borderS)
|
||||||
|
color: Color.mSurface
|
||||||
|
// Scale and fade-in animation
|
||||||
|
scale: scaleValue
|
||||||
|
opacity: opacityValue
|
||||||
|
// Animate in when the item is created
|
||||||
|
Component.onCompleted: {
|
||||||
|
scaleValue = 1;
|
||||||
|
opacityValue = 1;
|
||||||
|
}
|
||||||
|
// Check if this notification is being removed
|
||||||
|
onIsRemovingChanged: {
|
||||||
|
if (isRemoving)
|
||||||
|
removalTimer.start();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimized progress bar container
|
||||||
|
Rectangle {
|
||||||
|
id: progressBarContainer
|
||||||
|
|
||||||
|
// Pre-calculate available width for the progress bar
|
||||||
|
readonly property real availableWidth: parent.width - (2 * parent.radius)
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: 2
|
||||||
|
color: Color.transparent
|
||||||
|
|
||||||
|
// Actual progress bar - centered and symmetric
|
||||||
|
Rectangle {
|
||||||
|
id: progressBar
|
||||||
|
|
||||||
|
height: parent.height
|
||||||
|
// Center the bar and make it shrink symmetrically
|
||||||
|
x: parent.parent.radius + (parent.availableWidth * (1 - model.progress)) / 2
|
||||||
|
width: parent.availableWidth * model.progress
|
||||||
|
color: {
|
||||||
|
if (model.urgency === NotificationUrgency.Critical || model.urgency === 2)
|
||||||
|
return Colors.red;
|
||||||
|
else if (model.urgency === NotificationUrgency.Low || model.urgency === 0)
|
||||||
|
return Colors.green;
|
||||||
|
else
|
||||||
|
return Colors.primary;
|
||||||
|
}
|
||||||
|
antialiasing: true
|
||||||
|
|
||||||
|
// Smooth progress animation
|
||||||
|
Behavior on width {
|
||||||
|
enabled: !card.isRemoving // Disable during removal animation
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 100 // Quick but smooth
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
enabled: !card.isRemoving
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 100
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right-click to dismiss
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.RightButton)
|
||||||
|
animateOut();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer for delayed removal after animation
|
||||||
|
Timer {
|
||||||
|
id: removalTimer
|
||||||
|
|
||||||
|
interval: Style.animationSlow
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
NotificationService.dismissActiveNotification(notificationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: notificationLayout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginM
|
||||||
|
anchors.rightMargin: (Style.marginM + 32) // Leave space for close button
|
||||||
|
spacing: Style.marginM
|
||||||
|
|
||||||
|
// Main content section
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginM
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
// For real-time notification always show the original image
|
||||||
|
// as the cached version is most likely still processing.
|
||||||
|
NImageCircled {
|
||||||
|
Layout.preferredWidth: 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
Layout.topMargin: 30
|
||||||
|
imagePath: model.originalImage || ""
|
||||||
|
borderColor: Color.transparent
|
||||||
|
borderWidth: 0
|
||||||
|
fallbackIcon: "bell"
|
||||||
|
fallbackIconSize: 24
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text content
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
// Header section with app name and timestamp
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 6
|
||||||
|
Layout.preferredHeight: 6
|
||||||
|
radius: Style.radiusXS
|
||||||
|
color: {
|
||||||
|
if (model.urgency === NotificationUrgency.Critical || model.urgency === 2)
|
||||||
|
return Color.mError;
|
||||||
|
else if (model.urgency === NotificationUrgency.Low || model.urgency === 0)
|
||||||
|
return Color.mOnSurface;
|
||||||
|
else
|
||||||
|
return Color.mPrimary;
|
||||||
|
}
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: `${model.appName || I18n.tr("system.unknown-app")} · ${Time.formatRelativeTime(model.timestamp)}`
|
||||||
|
color: Color.mSecondary
|
||||||
|
pointSize: Style.fontSizeXS
|
||||||
|
family: Fonts.sans
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: model.summary || I18n.tr("general.no-summary")
|
||||||
|
pointSize: Style.fontSizeL
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
color: Color.mOnSurface
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
|
Layout.fillWidth: true
|
||||||
|
maximumLineCount: 3
|
||||||
|
elide: Text.ElideRight
|
||||||
|
family: Fonts.sans
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: model.body || ""
|
||||||
|
pointSize: Style.fontSizeM
|
||||||
|
color: Color.mOnSurface
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
|
Layout.fillWidth: true
|
||||||
|
maximumLineCount: 5
|
||||||
|
elide: Text.ElideRight
|
||||||
|
family: Fonts.sans
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification actions
|
||||||
|
Flow {
|
||||||
|
// Store the notification ID for access in button delegates
|
||||||
|
property string parentNotificationId: notificationId
|
||||||
|
// Parse actions from JSON string
|
||||||
|
property var parsedActions: {
|
||||||
|
try {
|
||||||
|
return model.actionsJson ? JSON.parse(model.actionsJson) : [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginS
|
||||||
|
Layout.topMargin: Style.marginM
|
||||||
|
flow: Flow.LeftToRight
|
||||||
|
layoutDirection: Qt.LeftToRight
|
||||||
|
visible: parsedActions.length > 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: parent.parsedActions
|
||||||
|
|
||||||
|
delegate: NButton {
|
||||||
|
property var actionData: modelData
|
||||||
|
|
||||||
|
text: {
|
||||||
|
var actionText = actionData.text || "Open";
|
||||||
|
// If text contains comma, take the part after the comma (the display text)
|
||||||
|
if (actionText.includes(","))
|
||||||
|
return actionText.split(",")[1] || actionText;
|
||||||
|
|
||||||
|
return actionText;
|
||||||
|
}
|
||||||
|
fontFamily: Fonts.sans
|
||||||
|
fontSize: Style.fontSizeS
|
||||||
|
backgroundColor: Color.mPrimary
|
||||||
|
textColor: hovered ? Color.mOnTertiary : Color.mOnPrimary
|
||||||
|
hoverColor: Color.mTertiary
|
||||||
|
outlined: false
|
||||||
|
implicitHeight: 24
|
||||||
|
onClicked: {
|
||||||
|
NotificationService.invokeAction(parent.parentNotificationId, actionData.identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close button positioned absolutely
|
||||||
|
NIconButton {
|
||||||
|
icon: "close"
|
||||||
|
baseSize: Style.baseWidgetSize * 0.6
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Style.marginM
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Style.marginM
|
||||||
|
onClicked: {
|
||||||
|
animateOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation behaviors
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutExpo
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -75,7 +75,7 @@ NPanel {
|
|||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "cloud"
|
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "cloud"
|
||||||
pointSize: Style.fontSizeXXL
|
pointSize: Style.fontSizeXXL
|
||||||
color: Color.mOnSurfaceVariant
|
color: Colors.text
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
@@ -91,7 +91,7 @@ NPanel {
|
|||||||
}
|
}
|
||||||
pointSize: Style.fontSizeM
|
pointSize: Style.fontSizeM
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
color: Color.mOnSurfaceVariant
|
color: Colors.text
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ NPanel {
|
|||||||
text: Time.date.getDate()
|
text: Time.date.getDate()
|
||||||
pointSize: Style.fontSizeXXXL * 1.5
|
pointSize: Style.fontSizeXXXL * 1.5
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
color: Color.mOnSurfaceVariant
|
color: Colors.text
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -123,7 +123,7 @@ NPanel {
|
|||||||
text: Qt.locale().monthName(grid.month, Locale.LongFormat).toUpperCase()
|
text: Qt.locale().monthName(grid.month, Locale.LongFormat).toUpperCase()
|
||||||
pointSize: Style.fontSizeXL * 1.2
|
pointSize: Style.fontSizeXL * 1.2
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
color: Color.mOnSurfaceVariant
|
color: Colors.text
|
||||||
Layout.alignment: Qt.AlignBaseline
|
Layout.alignment: Qt.AlignBaseline
|
||||||
Layout.maximumWidth: 150
|
Layout.maximumWidth: 150
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
@@ -133,7 +133,7 @@ NPanel {
|
|||||||
text: ` ${grid.year}`
|
text: ` ${grid.year}`
|
||||||
pointSize: Style.fontSizeL
|
pointSize: Style.fontSizeL
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
color: Qt.alpha(Color.mOnSurfaceVariant, 0.7)
|
color: Qt.alpha(Colors.text, 0.7)
|
||||||
Layout.alignment: Qt.AlignBaseline
|
Layout.alignment: Qt.AlignBaseline
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ NPanel {
|
|||||||
}
|
}
|
||||||
pointSize: Style.fontSizeM
|
pointSize: Style.fontSizeM
|
||||||
font.weight: Style.fontWeightMedium
|
font.weight: Style.fontWeightMedium
|
||||||
color: Color.mOnSurfaceVariant
|
color: Colors.text
|
||||||
Layout.maximumWidth: 150
|
Layout.maximumWidth: 150
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ NPanel {
|
|||||||
text: weatherReady ? ` (${LocationService.data.weather.timezone_abbreviation})` : ""
|
text: weatherReady ? ` (${LocationService.data.weather.timezone_abbreviation})` : ""
|
||||||
pointSize: Style.fontSizeXS
|
pointSize: Style.fontSizeXS
|
||||||
font.weight: Style.fontWeightMedium
|
font.weight: Style.fontWeightMedium
|
||||||
color: Qt.alpha(Color.mOnSurfaceVariant, 0.7)
|
color: Qt.alpha(Colors.text, 0.7)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -197,13 +197,13 @@ NPanel {
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
||||||
ctx.lineWidth = 2.5;
|
ctx.lineWidth = 2.5;
|
||||||
ctx.strokeStyle = Qt.alpha(Color.mOnSurfaceVariant, 0.15);
|
ctx.strokeStyle = Qt.alpha(Colors.text, 0.15);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
// Progress arc
|
// Progress arc
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI);
|
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI);
|
||||||
ctx.lineWidth = 2.5;
|
ctx.lineWidth = 2.5;
|
||||||
ctx.strokeStyle = Color.mOnSurfaceVariant;
|
ctx.strokeStyle = Colors.text;
|
||||||
ctx.lineCap = "round";
|
ctx.lineCap = "round";
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ NPanel {
|
|||||||
}
|
}
|
||||||
pointSize: Style.fontSizeXS
|
pointSize: Style.fontSizeXS
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
color: Color.mOnSurfaceVariant
|
color: Colors.text
|
||||||
family: Fonts.sans
|
family: Fonts.sans
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ NPanel {
|
|||||||
text: Qt.formatTime(Time.date, "mm")
|
text: Qt.formatTime(Time.date, "mm")
|
||||||
pointSize: Style.fontSizeXXS
|
pointSize: Style.fontSizeXXS
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
color: Color.mOnSurfaceVariant
|
color: Colors.text
|
||||||
family: Fonts.sans
|
family: Fonts.sans
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
@@ -298,7 +298,7 @@ NPanel {
|
|||||||
return `${max}°/${min}°`;
|
return `${max}°/${min}°`;
|
||||||
}
|
}
|
||||||
pointSize: Style.fontSizeXS
|
pointSize: Style.fontSizeXS
|
||||||
color: Color.mOnSurfaceVariant
|
color: Colors.text
|
||||||
font.weight: Style.fontWeightMedium
|
font.weight: Style.fontWeightMedium
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
344
quickshell/Modules/Panel/NotificationHistoryPanel.qml
Normal file
344
quickshell/Modules/Panel/NotificationHistoryPanel.qml
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Noctalia
|
||||||
|
import qs.Services
|
||||||
|
import qs.Utils
|
||||||
|
|
||||||
|
// Notification History panel
|
||||||
|
NPanel {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
preferredWidth: 380
|
||||||
|
preferredHeight: 480
|
||||||
|
onOpened: function() {
|
||||||
|
SettingsService.notifications.lastSeenTs = Time.timestamp * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
panelContent: Rectangle {
|
||||||
|
id: notificationRect
|
||||||
|
|
||||||
|
color: Color.transparent
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginL
|
||||||
|
spacing: Style.marginM
|
||||||
|
|
||||||
|
// Header section
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginM
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: "bell"
|
||||||
|
pointSize: Style.fontSizeXXL
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Notifications"
|
||||||
|
pointSize: Style.fontSizeL
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: SettingsService.notifications.doNotDisturb ? "bell-off" : "bell"
|
||||||
|
baseSize: Style.baseWidgetSize * 0.8
|
||||||
|
onClicked: SettingsService.notifications.doNotDisturb = !SettingsService.notifications.doNotDisturb
|
||||||
|
colorFg: SettingsService.notifications.doNotDisturb ? Colors.base : Colors.green
|
||||||
|
colorBg: SettingsService.notifications.doNotDisturb ? Colors.green : Color.transparent
|
||||||
|
colorFgHover: Colors.base
|
||||||
|
colorBgHover: Colors.green
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "trash"
|
||||||
|
baseSize: Style.baseWidgetSize * 0.8
|
||||||
|
onClicked: {
|
||||||
|
NotificationService.clearHistory();
|
||||||
|
// Close panel as there is nothing more to see.
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
colorFg: Colors.red
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorFgHover: Colors.base
|
||||||
|
colorBgHover: Colors.red
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "close"
|
||||||
|
baseSize: Style.baseWidgetSize * 0.8
|
||||||
|
onClicked: root.close()
|
||||||
|
colorFg: Colors.blue
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorFgHover: Colors.base
|
||||||
|
colorBgHover: Colors.blue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
NDivider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state when no notifications
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
visible: NotificationService.historyList.count === 0
|
||||||
|
spacing: Style.marginL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: "bell-off"
|
||||||
|
pointSize: 64
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "No Notifications"
|
||||||
|
pointSize: Style.fontSizeL
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification list
|
||||||
|
NListView {
|
||||||
|
id: notificationList
|
||||||
|
|
||||||
|
// Track which notification is expanded
|
||||||
|
property string expandedId: ""
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
horizontalPolicy: ScrollBar.AlwaysOff
|
||||||
|
verticalPolicy: ScrollBar.AsNeeded
|
||||||
|
model: NotificationService.historyList
|
||||||
|
spacing: Style.marginM
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
visible: NotificationService.historyList.count > 0
|
||||||
|
|
||||||
|
delegate: NBox {
|
||||||
|
property string notificationId: model.id
|
||||||
|
property bool isExpanded: notificationList.expandedId === notificationId
|
||||||
|
|
||||||
|
width: notificationList.width
|
||||||
|
height: notificationLayout.implicitHeight + (Style.marginM * 2)
|
||||||
|
|
||||||
|
// Click to expand/collapse
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
// Don't capture clicks on the delete button
|
||||||
|
anchors.rightMargin: 48
|
||||||
|
enabled: (summaryText.truncated || bodyText.truncated)
|
||||||
|
onClicked: {
|
||||||
|
if (notificationList.expandedId === notificationId)
|
||||||
|
notificationList.expandedId = "";
|
||||||
|
else
|
||||||
|
notificationList.expandedId = notificationId;
|
||||||
|
}
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: notificationLayout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginM
|
||||||
|
spacing: Style.marginM
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
NImageCircled {
|
||||||
|
Layout.preferredWidth: 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
Layout.topMargin: 20
|
||||||
|
imagePath: model.cachedImage || model.originalImage || ""
|
||||||
|
borderColor: Color.transparent
|
||||||
|
borderWidth: 0
|
||||||
|
fallbackIcon: "bell"
|
||||||
|
fallbackIconSize: 24
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification content column
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
spacing: Style.marginXS
|
||||||
|
Layout.rightMargin: -(Style.marginM + Style.baseWidgetSize * 0.6)
|
||||||
|
|
||||||
|
// Header row with app name and timestamp
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
|
// Urgency indicator
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 6
|
||||||
|
Layout.preferredHeight: 6
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
radius: 3
|
||||||
|
visible: model.urgency !== 1
|
||||||
|
color: {
|
||||||
|
if (model.urgency === 2)
|
||||||
|
return Color.mError;
|
||||||
|
else if (model.urgency === 0)
|
||||||
|
return Color.mOnSurfaceVariant;
|
||||||
|
else
|
||||||
|
return Color.transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: model.appName || "Unknown App"
|
||||||
|
pointSize: Style.fontSizeXS
|
||||||
|
color: Color.mSecondary
|
||||||
|
family: Fonts.sans
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: Time.formatRelativeTime(model.timestamp)
|
||||||
|
pointSize: Style.fontSizeXS
|
||||||
|
color: Color.mSecondary
|
||||||
|
family: Fonts.sans
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
NText {
|
||||||
|
id: summaryText
|
||||||
|
|
||||||
|
text: model.summary || "No Summary"
|
||||||
|
pointSize: Style.fontSizeM
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Color.mOnSurface
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
maximumLineCount: isExpanded ? 999 : 2
|
||||||
|
family: Fonts.sans
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body
|
||||||
|
NText {
|
||||||
|
id: bodyText
|
||||||
|
|
||||||
|
text: model.body || ""
|
||||||
|
pointSize: Style.fontSizeS
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
maximumLineCount: isExpanded ? 999 : 3
|
||||||
|
elide: Text.ElideRight
|
||||||
|
family: Fonts.sans
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer for expand indicator
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: (!isExpanded && (summaryText.truncated || bodyText.truncated)) ? (Style.marginS) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand indicator
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: !isExpanded && (summaryText.truncated || bodyText.truncated)
|
||||||
|
spacing: Style.marginXS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Click to expand"
|
||||||
|
pointSize: Style.fontSizeXS
|
||||||
|
color: Color.mPrimary
|
||||||
|
family: Fonts.sans
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: "chevron-down"
|
||||||
|
pointSize: Style.fontSizeS
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
NIconButton {
|
||||||
|
icon: "trash"
|
||||||
|
baseSize: Style.baseWidgetSize * 0.7
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
onClicked: {
|
||||||
|
// Remove from history using the service API
|
||||||
|
NotificationService.removeFromHistory(notificationId);
|
||||||
|
}
|
||||||
|
colorFg: Colors.red
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorFgHover: Colors.base
|
||||||
|
colorBgHover: Colors.red
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth color transition on hover
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
183
quickshell/Noctalia/NButton.qml
Normal file
183
quickshell/Noctalia/NButton.qml
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Constants
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Public properties
|
||||||
|
property string text: ""
|
||||||
|
property string icon: ""
|
||||||
|
property string tooltipText
|
||||||
|
property color backgroundColor: Color.mPrimary
|
||||||
|
property color textColor: Color.mOnPrimary
|
||||||
|
property color hoverColor: Color.mTertiary
|
||||||
|
property bool enabled: true
|
||||||
|
property real fontSize: Style.fontSizeM * scaling
|
||||||
|
property int fontWeight: Style.fontWeightBold
|
||||||
|
property string fontFamily: Fonts.primary
|
||||||
|
property real iconSize: Style.fontSizeL * scaling
|
||||||
|
property bool outlined: false
|
||||||
|
// Internal properties
|
||||||
|
property bool hovered: false
|
||||||
|
property bool pressed: false
|
||||||
|
|
||||||
|
// Signals
|
||||||
|
signal clicked()
|
||||||
|
signal rightClicked()
|
||||||
|
signal middleClicked()
|
||||||
|
|
||||||
|
// Dimensions
|
||||||
|
implicitWidth: contentRow.implicitWidth + (Style.marginL * 2 * scaling)
|
||||||
|
implicitHeight: Math.max(Style.baseWidgetSize * scaling, contentRow.implicitHeight + (Style.marginM * scaling))
|
||||||
|
// Appearance
|
||||||
|
radius: Style.radiusS * scaling
|
||||||
|
color: {
|
||||||
|
if (!enabled)
|
||||||
|
return outlined ? Color.transparent : Qt.lighter(Color.mSurfaceVariant, 1.2);
|
||||||
|
|
||||||
|
if (hovered)
|
||||||
|
return hoverColor;
|
||||||
|
|
||||||
|
return outlined ? Color.transparent : backgroundColor;
|
||||||
|
}
|
||||||
|
border.width: outlined ? Math.max(1, Style.borderS * scaling) : 0
|
||||||
|
border.color: {
|
||||||
|
if (!enabled)
|
||||||
|
return Color.mOutline;
|
||||||
|
|
||||||
|
if (pressed || hovered)
|
||||||
|
return backgroundColor;
|
||||||
|
|
||||||
|
return outlined ? backgroundColor : Color.transparent;
|
||||||
|
}
|
||||||
|
opacity: enabled ? 1 : 0.6
|
||||||
|
|
||||||
|
// Content
|
||||||
|
RowLayout {
|
||||||
|
id: contentRow
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Style.marginXS * scaling
|
||||||
|
|
||||||
|
// Icon (optional)
|
||||||
|
NIcon {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
visible: root.icon !== ""
|
||||||
|
icon: root.icon
|
||||||
|
pointSize: root.iconSize
|
||||||
|
color: {
|
||||||
|
if (!root.enabled)
|
||||||
|
return Color.mOnSurfaceVariant;
|
||||||
|
|
||||||
|
if (root.outlined) {
|
||||||
|
if (root.pressed || root.hovered)
|
||||||
|
return root.backgroundColor;
|
||||||
|
|
||||||
|
return root.backgroundColor;
|
||||||
|
}
|
||||||
|
return root.textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text
|
||||||
|
NText {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
visible: root.text !== ""
|
||||||
|
text: root.text
|
||||||
|
pointSize: root.fontSize
|
||||||
|
font.weight: root.fontWeight
|
||||||
|
family: root.fontFamily
|
||||||
|
color: {
|
||||||
|
if (!root.enabled)
|
||||||
|
return Color.mOnSurfaceVariant;
|
||||||
|
|
||||||
|
if (root.outlined) {
|
||||||
|
if (root.hovered)
|
||||||
|
return root.textColor;
|
||||||
|
|
||||||
|
return root.backgroundColor;
|
||||||
|
}
|
||||||
|
return root.textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse interaction
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: root.enabled
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onEntered: {
|
||||||
|
root.hovered = true;
|
||||||
|
if (tooltipText)
|
||||||
|
TooltipService.show(Screen, root, root.tooltipText);
|
||||||
|
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
root.hovered = false;
|
||||||
|
if (tooltipText)
|
||||||
|
TooltipService.hide();
|
||||||
|
|
||||||
|
}
|
||||||
|
onPressed: (mouse) => {
|
||||||
|
if (tooltipText)
|
||||||
|
TooltipService.hide();
|
||||||
|
|
||||||
|
if (mouse.button === Qt.LeftButton)
|
||||||
|
root.clicked();
|
||||||
|
else if (mouse.button == Qt.RightButton)
|
||||||
|
root.rightClicked();
|
||||||
|
else if (mouse.button == Qt.MiddleButton)
|
||||||
|
root.middleClicked();
|
||||||
|
}
|
||||||
|
onCanceled: {
|
||||||
|
root.hovered = false;
|
||||||
|
if (tooltipText)
|
||||||
|
TooltipService.hide();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -118,10 +118,10 @@ Singleton {
|
|||||||
function _fetchWeather(latitude, longitude, errorCallback) {
|
function _fetchWeather(latitude, longitude, errorCallback) {
|
||||||
Logger.log("Location", "Fetching weather from api.open-meteo.com");
|
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 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";
|
||||||
curl.fetch(url, function(success, data) {
|
curl.fetch(url, function(success, fetchedData) {
|
||||||
if (success) {
|
if (success) {
|
||||||
try {
|
try {
|
||||||
var weatherData = JSON.parse(data);
|
var weatherData = JSON.parse(fetchedData);
|
||||||
// Save core data
|
// Save core data
|
||||||
data.weather = weatherData;
|
data.weather = weatherData;
|
||||||
data.weatherLastFetch = Time.timestamp;
|
data.weatherLastFetch = Time.timestamp;
|
||||||
|
|||||||
474
quickshell/Services/NotificationService.qml
Normal file
474
quickshell/Services/NotificationService.qml
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Window
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import qs.Utils
|
||||||
|
import qs.Services
|
||||||
|
import qs.Constants
|
||||||
|
import "../Utils/sha256.js" as Checksum
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
property int maxVisible: 5
|
||||||
|
property int maxHistory: 100
|
||||||
|
property string historyFile: Qt.resolvedUrl("../Assets/Config/Notifications.json")
|
||||||
|
property string cacheDirImagesNotifications: Quickshell.env("HOME") + "/.cache/quickshell/notifications/"
|
||||||
|
property real lowUrgencyDuration: 3
|
||||||
|
property real normalUrgencyDuration: 8
|
||||||
|
property real criticalUrgencyDuration: 15
|
||||||
|
|
||||||
|
// Models
|
||||||
|
property ListModel activeList: ListModel {}
|
||||||
|
property ListModel historyList: ListModel {}
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
property var activeMap: ({})
|
||||||
|
property var imageQueue: []
|
||||||
|
|
||||||
|
// Performance optimization: Track notification metadata separately
|
||||||
|
property var notificationMetadata: ({}) // Stores timestamp and duration for each notification
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
implicitHeight: 1
|
||||||
|
implicitWidth: 1
|
||||||
|
color: Color.transparent
|
||||||
|
mask: Region {}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: cacher
|
||||||
|
width: 64
|
||||||
|
height: 64
|
||||||
|
visible: true
|
||||||
|
cache: false
|
||||||
|
asynchronous: true
|
||||||
|
mipmap: true
|
||||||
|
antialiasing: true
|
||||||
|
|
||||||
|
onStatusChanged: {
|
||||||
|
if (imageQueue.length === 0)
|
||||||
|
return
|
||||||
|
const req = imageQueue[0]
|
||||||
|
|
||||||
|
if (status === Image.Ready) {
|
||||||
|
Quickshell.execDetached(["mkdir", "-p", cacheDirImagesNotifications])
|
||||||
|
grabToImage(result => {
|
||||||
|
if (result.saveToFile(req.dest))
|
||||||
|
updateImagePath(req.imageId, req.dest)
|
||||||
|
processNextImage()
|
||||||
|
})
|
||||||
|
} else if (status === Image.Error) {
|
||||||
|
processNextImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processNextImage() {
|
||||||
|
imageQueue.shift()
|
||||||
|
if (imageQueue.length > 0) {
|
||||||
|
source = imageQueue[0].src
|
||||||
|
} else {
|
||||||
|
source = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification server
|
||||||
|
NotificationServer {
|
||||||
|
keepOnReload: false
|
||||||
|
imageSupported: true
|
||||||
|
actionsSupported: true
|
||||||
|
onNotification: notification => handleNotification(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main handler
|
||||||
|
function handleNotification(notification) {
|
||||||
|
const data = createData(notification)
|
||||||
|
addToHistory(data)
|
||||||
|
|
||||||
|
if (SettingsService.notifications.doNotDisturb)
|
||||||
|
return
|
||||||
|
|
||||||
|
activeMap[data.id] = notification
|
||||||
|
notification.tracked = true
|
||||||
|
notification.closed.connect(() => removeActive(data.id))
|
||||||
|
|
||||||
|
// Store metadata for efficient progress calculation
|
||||||
|
const durations = [lowUrgencyDuration * 1000, normalUrgencyDuration * 1000, criticalUrgencyDuration * 1000]
|
||||||
|
|
||||||
|
let expire = 0
|
||||||
|
if (data.expireTimeout === 0) {
|
||||||
|
expire = -1 // Never expire
|
||||||
|
} else if (data.expireTimeout > 0) {
|
||||||
|
expire = data.expireTimeout
|
||||||
|
} else {
|
||||||
|
expire = durations[data.urgency]
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationMetadata[data.id] = {
|
||||||
|
"timestamp": data.timestamp.getTime(),
|
||||||
|
"duration": expire,
|
||||||
|
"urgency": data.urgency
|
||||||
|
}
|
||||||
|
|
||||||
|
activeList.insert(0, data)
|
||||||
|
while (activeList.count > maxVisible) {
|
||||||
|
const last = activeList.get(activeList.count - 1)
|
||||||
|
activeMap[last.id]?.dismiss()
|
||||||
|
activeList.remove(activeList.count - 1)
|
||||||
|
delete notificationMetadata[last.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createData(n) {
|
||||||
|
const time = new Date()
|
||||||
|
const id = Checksum.sha256(JSON.stringify({
|
||||||
|
"summary": n.summary,
|
||||||
|
"body": n.body,
|
||||||
|
"app": n.appName,
|
||||||
|
"time": time.getTime()
|
||||||
|
}))
|
||||||
|
|
||||||
|
const image = n.image || getIcon(n.appIcon)
|
||||||
|
const imageId = generateImageId(n, image)
|
||||||
|
queueImage(image, imageId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": id,
|
||||||
|
"summary": (n.summary || ""),
|
||||||
|
"body": stripTags(n.body || ""),
|
||||||
|
"appName": getAppName(n.appName),
|
||||||
|
"urgency": n.urgency < 0 || n.urgency > 2 ? 1 : n.urgency,
|
||||||
|
"expireTimeout": n.expireTimeout,
|
||||||
|
"timestamp": time,
|
||||||
|
"progress": 1.0,
|
||||||
|
"originalImage": image,
|
||||||
|
"cachedImage": imageId ? (cacheDirImagesNotifications + imageId + ".png") : image,
|
||||||
|
"actionsJson": JSON.stringify((n.actions || []).map(a => ({
|
||||||
|
"text": a.text || "Action",
|
||||||
|
"identifier": a.identifier || ""
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueImage(path, imageId) {
|
||||||
|
if (!path || !path.startsWith("image://") || !imageId)
|
||||||
|
return
|
||||||
|
|
||||||
|
const dest = cacheDirImagesNotifications + imageId + ".png"
|
||||||
|
|
||||||
|
for (const req of imageQueue) {
|
||||||
|
if (req.imageId === imageId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imageQueue.push({
|
||||||
|
"src": path,
|
||||||
|
"dest": dest,
|
||||||
|
"imageId": imageId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (imageQueue.length === 1)
|
||||||
|
cacher.source = path
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateImagePath(id, path) {
|
||||||
|
updateModel(activeList, id, "cachedImage", path)
|
||||||
|
updateModel(historyList, id, "cachedImage", path)
|
||||||
|
saveHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateModel(model, id, prop, value) {
|
||||||
|
for (var i = 0; i < model.count; i++) {
|
||||||
|
if (model.get(i).id === id) {
|
||||||
|
model.setProperty(i, prop, value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeActive(id) {
|
||||||
|
for (var i = 0; i < activeList.count; i++) {
|
||||||
|
if (activeList.get(i).id === id) {
|
||||||
|
activeList.remove(i)
|
||||||
|
delete activeMap[id]
|
||||||
|
delete notificationMetadata[id]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimized batch progress update
|
||||||
|
Timer {
|
||||||
|
interval: 50 // Reduced from 10ms to 50ms (20 updates/sec instead of 100)
|
||||||
|
repeat: true
|
||||||
|
running: activeList.count > 0
|
||||||
|
onTriggered: updateAllProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAllProgress() {
|
||||||
|
const now = Date.now()
|
||||||
|
const toRemove = []
|
||||||
|
const updates = [] // Batch updates
|
||||||
|
|
||||||
|
// Collect all updates first
|
||||||
|
for (var i = 0; i < activeList.count; i++) {
|
||||||
|
const notif = activeList.get(i)
|
||||||
|
const meta = notificationMetadata[notif.id]
|
||||||
|
|
||||||
|
if (!meta || meta.duration === -1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
// Skip infinite notifications
|
||||||
|
const elapsed = now - meta.timestamp
|
||||||
|
const progress = Math.max(1.0 - (elapsed / meta.duration), 0.0)
|
||||||
|
|
||||||
|
if (progress <= 0) {
|
||||||
|
toRemove.push(notif.id)
|
||||||
|
} else if (Math.abs(notif.progress - progress) > 0.005) {
|
||||||
|
// Only update if change is significant
|
||||||
|
updates.push({
|
||||||
|
"index": i,
|
||||||
|
"progress": progress
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply batch updates
|
||||||
|
for (const update of updates) {
|
||||||
|
activeList.setProperty(update.index, "progress", update.progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove expired notifications (one at a time to allow animation)
|
||||||
|
if (toRemove.length > 0) {
|
||||||
|
animateAndRemove(toRemove[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// History management
|
||||||
|
function addToHistory(data) {
|
||||||
|
historyList.insert(0, data)
|
||||||
|
|
||||||
|
while (historyList.count > maxHistory) {
|
||||||
|
const old = historyList.get(historyList.count - 1)
|
||||||
|
if (old.cachedImage && !old.cachedImage.startsWith("image://")) {
|
||||||
|
Quickshell.execDetached(["rm", "-f", old.cachedImage])
|
||||||
|
}
|
||||||
|
historyList.remove(historyList.count - 1)
|
||||||
|
}
|
||||||
|
saveHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persistence
|
||||||
|
FileView {
|
||||||
|
id: historyFileView
|
||||||
|
path: historyFile
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: loadHistory()
|
||||||
|
onLoadFailed: error => {
|
||||||
|
if (error === 2)
|
||||||
|
writeAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonAdapter {
|
||||||
|
id: adapter
|
||||||
|
property var notifications: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: saveTimer
|
||||||
|
interval: 200
|
||||||
|
onTriggered: performSaveHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveHistory() {
|
||||||
|
saveTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function performSaveHistory() {
|
||||||
|
try {
|
||||||
|
const items = []
|
||||||
|
for (var i = 0; i < historyList.count; i++) {
|
||||||
|
const n = historyList.get(i)
|
||||||
|
const copy = Object.assign({}, n)
|
||||||
|
copy.timestamp = n.timestamp.getTime()
|
||||||
|
items.push(copy)
|
||||||
|
}
|
||||||
|
adapter.notifications = items
|
||||||
|
historyFileView.writeAdapter()
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error("Notifications", "Save history failed:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHistory() {
|
||||||
|
try {
|
||||||
|
historyList.clear()
|
||||||
|
for (const item of adapter.notifications || []) {
|
||||||
|
const time = new Date(item.timestamp)
|
||||||
|
|
||||||
|
let cachedImage = item.cachedImage || ""
|
||||||
|
if (item.originalImage && item.originalImage.startsWith("image://") && !cachedImage) {
|
||||||
|
const imageId = generateImageId(item, item.originalImage)
|
||||||
|
if (imageId) {
|
||||||
|
cachedImage = cacheDirImagesNotifications + imageId + ".png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
historyList.append({
|
||||||
|
"id": item.id || "",
|
||||||
|
"summary": item.summary || "",
|
||||||
|
"body": item.body || "",
|
||||||
|
"appName": item.appName || "",
|
||||||
|
"urgency": item.urgency < 0 || item.urgency > 2 ? 1 : item.urgency,
|
||||||
|
"timestamp": time,
|
||||||
|
"originalImage": item.originalImage || "",
|
||||||
|
"cachedImage": cachedImage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error("Notifications", "Load failed:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppName(name) {
|
||||||
|
if (!name || name.trim() === "")
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
name = name.trim()
|
||||||
|
|
||||||
|
if (name.includes(".") && (name.startsWith("com.") || name.startsWith("org.") || name.startsWith("io.") || name.startsWith("net."))) {
|
||||||
|
const parts = name.split(".")
|
||||||
|
let appPart = parts[parts.length - 1]
|
||||||
|
|
||||||
|
if (!appPart || appPart === "app" || appPart === "desktop") {
|
||||||
|
appPart = parts[parts.length - 2] || parts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appPart) {
|
||||||
|
name = appPart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.includes(".")) {
|
||||||
|
const parts = name.split(".")
|
||||||
|
let displayName = parts[parts.length - 1]
|
||||||
|
|
||||||
|
if (!displayName || /^\d+$/.test(displayName)) {
|
||||||
|
displayName = parts[parts.length - 2] || parts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayName) {
|
||||||
|
displayName = displayName.charAt(0).toUpperCase() + displayName.slice(1)
|
||||||
|
displayName = displayName.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||||
|
displayName = displayName.replace(/app$/i, '').trim()
|
||||||
|
displayName = displayName.replace(/desktop$/i, '').trim()
|
||||||
|
displayName = displayName.replace(/flatpak$/i, '').trim()
|
||||||
|
|
||||||
|
if (!displayName) {
|
||||||
|
displayName = parts[parts.length - 1].charAt(0).toUpperCase() + parts[parts.length - 1].slice(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayName || name
|
||||||
|
}
|
||||||
|
|
||||||
|
let displayName = name.charAt(0).toUpperCase() + name.slice(1)
|
||||||
|
displayName = displayName.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||||
|
displayName = displayName.replace(/app$/i, '').trim()
|
||||||
|
displayName = displayName.replace(/desktop$/i, '').trim()
|
||||||
|
|
||||||
|
return displayName || name
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIcon(icon) {
|
||||||
|
if (!icon)
|
||||||
|
return ""
|
||||||
|
if (icon.startsWith("/") || icon.startsWith("file://"))
|
||||||
|
return icon
|
||||||
|
return ThemeIcons.iconFromName(icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripTags(text) {
|
||||||
|
return text.replace(/<[^>]*>?/gm, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateImageId(notification, image) {
|
||||||
|
if (image && image.startsWith("image://")) {
|
||||||
|
if (image.startsWith("image://qsimage/")) {
|
||||||
|
const key = (notification.appName || "") + "|" + (notification.summary || "")
|
||||||
|
return Checksum.sha256(key)
|
||||||
|
}
|
||||||
|
return Checksum.sha256(image)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
function dismissActiveNotification(id) {
|
||||||
|
activeMap[id]?.dismiss()
|
||||||
|
removeActive(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissAllActive() {
|
||||||
|
Object.values(activeMap).forEach(n => n.dismiss())
|
||||||
|
activeList.clear()
|
||||||
|
activeMap = {}
|
||||||
|
notificationMetadata = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokeAction(id, actionId) {
|
||||||
|
const n = activeMap[id]
|
||||||
|
if (!n?.actions)
|
||||||
|
return false
|
||||||
|
|
||||||
|
for (const action of n.actions) {
|
||||||
|
if (action.identifier === actionId && action.invoke) {
|
||||||
|
action.invoke()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromHistory(notificationId) {
|
||||||
|
for (var i = 0; i < historyList.count; i++) {
|
||||||
|
const notif = historyList.get(i)
|
||||||
|
if (notif.id === notificationId) {
|
||||||
|
if (notif.cachedImage && !notif.cachedImage.startsWith("image://")) {
|
||||||
|
Quickshell.execDetached(["rm", "-f", notif.cachedImage])
|
||||||
|
}
|
||||||
|
historyList.remove(i)
|
||||||
|
saveHistory()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHistory() {
|
||||||
|
try {
|
||||||
|
Quickshell.execDetached(["sh", "-c", `rm -rf "${cacheDirImagesNotifications}"*`])
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error("Notifications", "Failed to clear cache directory:", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
historyList.clear()
|
||||||
|
saveHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signals & connections
|
||||||
|
signal animateAndRemove(string notificationId)
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SettingsService.notifications
|
||||||
|
onDoNotDisturbChanged: {
|
||||||
|
const enabled = SettingsService.notifications.doNotDisturb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,16 +8,9 @@ Singleton {
|
|||||||
|
|
||||||
function show(title, message, timeout = 5000, icon = "", urgency = "normal") {
|
function show(title, message, timeout = 5000, icon = "", urgency = "normal") {
|
||||||
if (icon)
|
if (icon)
|
||||||
action.command = ["notify-send", "-u", urgency, "-i", icon, "-t", timeout.toString(), title, message];
|
Quickshell.execDetached(["notify-send", "-u", urgency, "-i", icon, "-t", timeout.toString(), title, message, "-a", "quickshell"]);
|
||||||
else
|
else
|
||||||
action.command = ["notify-send", "-u", urgency, "-t", timeout.toString(), title, message];
|
Quickshell.execDetached(["notify-send", "-u", urgency, "-t", timeout.toString(), title, message, "-a", "quickshell"]);
|
||||||
action.startDetached();
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: action
|
|
||||||
|
|
||||||
running: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pragma Singleton
|
|||||||
Singleton {
|
Singleton {
|
||||||
property alias primaryColor: adapter.primaryColor
|
property alias primaryColor: adapter.primaryColor
|
||||||
property alias showLyricsBar: adapter.showLyricsBar
|
property alias showLyricsBar: adapter.showLyricsBar
|
||||||
|
property alias notifications: adapter.notifications
|
||||||
property string settingsFilePath: Qt.resolvedUrl("../Assets/Config/Settings.json")
|
property string settingsFilePath: Qt.resolvedUrl("../Assets/Config/Settings.json")
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
@@ -16,20 +17,22 @@ Singleton {
|
|||||||
path: settingsFilePath
|
path: settingsFilePath
|
||||||
watchChanges: true
|
watchChanges: true
|
||||||
onFileChanged: reload()
|
onFileChanged: reload()
|
||||||
|
onAdapterUpdated: writeAdapter()
|
||||||
|
|
||||||
JsonAdapter {
|
JsonAdapter {
|
||||||
id: adapter
|
id: adapter
|
||||||
|
|
||||||
property string primaryColor: "#89b4fa"
|
property string primaryColor: "#89b4fa"
|
||||||
property bool showLyricsBar: false
|
property bool showLyricsBar: false
|
||||||
|
property JsonObject notifications
|
||||||
|
|
||||||
|
notifications: JsonObject {
|
||||||
|
property bool doNotDisturb: false
|
||||||
|
property real lastSeenTs: 0
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: adapter
|
|
||||||
onPrimaryColorChanged: settingsFile.writeAdapter()
|
|
||||||
onShowLyricsBarChanged: settingsFile.writeAdapter()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
192
quickshell/Utils/sha256.js
Normal file
192
quickshell/Utils/sha256.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
function sha256(message) {
|
||||||
|
// SHA-256 constants
|
||||||
|
const K = [
|
||||||
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||||
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||||
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||||
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||||
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||||
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||||
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||||
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
||||||
|
];
|
||||||
|
|
||||||
|
// Initial hash values
|
||||||
|
let h0 = 0x6a09e667;
|
||||||
|
let h1 = 0xbb67ae85;
|
||||||
|
let h2 = 0x3c6ef372;
|
||||||
|
let h3 = 0xa54ff53a;
|
||||||
|
let h4 = 0x510e527f;
|
||||||
|
let h5 = 0x9b05688c;
|
||||||
|
let h6 = 0x1f83d9ab;
|
||||||
|
let h7 = 0x5be0cd19;
|
||||||
|
|
||||||
|
// Convert string to UTF-8 bytes manually
|
||||||
|
const msgBytes = stringToUtf8Bytes(message);
|
||||||
|
const msgLength = msgBytes.length;
|
||||||
|
const bitLength = msgLength * 8;
|
||||||
|
|
||||||
|
// Calculate padding
|
||||||
|
// Message + 1 bit (0x80) + padding zeros + 8 bytes for length = multiple of 64 bytes
|
||||||
|
const totalBitsNeeded = bitLength + 1 + 64; // message bits + padding bit + 64-bit length
|
||||||
|
const totalBytesNeeded = Math.ceil(totalBitsNeeded / 8);
|
||||||
|
const paddedLength = Math.ceil(totalBytesNeeded / 64) * 64; // Round up to multiple of 64
|
||||||
|
|
||||||
|
const paddedMsg = new Array(paddedLength).fill(0);
|
||||||
|
|
||||||
|
// Copy original message
|
||||||
|
for (let i = 0; i < msgLength; i++) {
|
||||||
|
paddedMsg[i] = msgBytes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add padding bit (0x80 = 10000000 in binary)
|
||||||
|
paddedMsg[msgLength] = 0x80;
|
||||||
|
|
||||||
|
// Add length as 64-bit big-endian integer at the end
|
||||||
|
// JavaScript numbers are not precise enough for 64-bit integers, so we handle high/low separately
|
||||||
|
const highBits = Math.floor(bitLength / 0x100000000);
|
||||||
|
const lowBits = bitLength % 0x100000000;
|
||||||
|
|
||||||
|
// Write 64-bit length in big-endian format
|
||||||
|
paddedMsg[paddedLength - 8] = (highBits >>> 24) & 0xFF;
|
||||||
|
paddedMsg[paddedLength - 7] = (highBits >>> 16) & 0xFF;
|
||||||
|
paddedMsg[paddedLength - 6] = (highBits >>> 8) & 0xFF;
|
||||||
|
paddedMsg[paddedLength - 5] = highBits & 0xFF;
|
||||||
|
paddedMsg[paddedLength - 4] = (lowBits >>> 24) & 0xFF;
|
||||||
|
paddedMsg[paddedLength - 3] = (lowBits >>> 16) & 0xFF;
|
||||||
|
paddedMsg[paddedLength - 2] = (lowBits >>> 8) & 0xFF;
|
||||||
|
paddedMsg[paddedLength - 1] = lowBits & 0xFF;
|
||||||
|
|
||||||
|
// Process message in 512-bit (64-byte) chunks
|
||||||
|
for (let chunk = 0; chunk < paddedLength; chunk += 64) {
|
||||||
|
const w = new Array(64);
|
||||||
|
|
||||||
|
// Break chunk into sixteen 32-bit big-endian words
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
const offset = chunk + i * 4;
|
||||||
|
w[i] = (paddedMsg[offset] << 24) |
|
||||||
|
(paddedMsg[offset + 1] << 16) |
|
||||||
|
(paddedMsg[offset + 2] << 8) |
|
||||||
|
paddedMsg[offset + 3];
|
||||||
|
// Ensure unsigned 32-bit
|
||||||
|
w[i] = w[i] >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend the sixteen 32-bit words into sixty-four 32-bit words
|
||||||
|
for (let i = 16; i < 64; i++) {
|
||||||
|
const s0 = rightRotate(w[i - 15], 7) ^ rightRotate(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
||||||
|
const s1 = rightRotate(w[i - 2], 17) ^ rightRotate(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
||||||
|
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize working variables for this chunk
|
||||||
|
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
|
||||||
|
|
||||||
|
// Main loop
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
const S1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
|
||||||
|
const ch = (e & f) ^ (~e & g);
|
||||||
|
const temp1 = (h + S1 + ch + K[i] + w[i]) >>> 0;
|
||||||
|
const S0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
|
||||||
|
const maj = (a & b) ^ (a & c) ^ (b & c);
|
||||||
|
const temp2 = (S0 + maj) >>> 0;
|
||||||
|
|
||||||
|
h = g;
|
||||||
|
g = f;
|
||||||
|
f = e;
|
||||||
|
e = (d + temp1) >>> 0;
|
||||||
|
d = c;
|
||||||
|
c = b;
|
||||||
|
b = a;
|
||||||
|
a = (temp1 + temp2) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this chunk's hash to result so far
|
||||||
|
h0 = (h0 + a) >>> 0;
|
||||||
|
h1 = (h1 + b) >>> 0;
|
||||||
|
h2 = (h2 + c) >>> 0;
|
||||||
|
h3 = (h3 + d) >>> 0;
|
||||||
|
h4 = (h4 + e) >>> 0;
|
||||||
|
h5 = (h5 + f) >>> 0;
|
||||||
|
h6 = (h6 + g) >>> 0;
|
||||||
|
h7 = (h7 + h) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produce the final hash value as a hex string
|
||||||
|
return [h0, h1, h2, h3, h4, h5, h6, h7]
|
||||||
|
.map(h => h.toString(16).padStart(8, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringToUtf8Bytes(str) {
|
||||||
|
const bytes = [];
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
let code = str.charCodeAt(i);
|
||||||
|
|
||||||
|
if (code < 0x80) {
|
||||||
|
// 1-byte character (ASCII)
|
||||||
|
bytes.push(code);
|
||||||
|
} else if (code < 0x800) {
|
||||||
|
// 2-byte character
|
||||||
|
bytes.push(0xC0 | (code >> 6));
|
||||||
|
bytes.push(0x80 | (code & 0x3F));
|
||||||
|
} else if (code < 0xD800 || code > 0xDFFF) {
|
||||||
|
// 3-byte character (not surrogate)
|
||||||
|
bytes.push(0xE0 | (code >> 12));
|
||||||
|
bytes.push(0x80 | ((code >> 6) & 0x3F));
|
||||||
|
bytes.push(0x80 | (code & 0x3F));
|
||||||
|
} else {
|
||||||
|
// 4-byte character (surrogate pair)
|
||||||
|
i++; // Move to next character
|
||||||
|
const code2 = str.charCodeAt(i);
|
||||||
|
const codePoint = 0x10000 + (((code & 0x3FF) << 10) | (code2 & 0x3FF));
|
||||||
|
bytes.push(0xF0 | (codePoint >> 18));
|
||||||
|
bytes.push(0x80 | ((codePoint >> 12) & 0x3F));
|
||||||
|
bytes.push(0x80 | ((codePoint >> 6) & 0x3F));
|
||||||
|
bytes.push(0x80 | (codePoint & 0x3F));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rightRotate(value, amount) {
|
||||||
|
return ((value >>> amount) | (value << (32 - amount))) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test function to verify implementation
|
||||||
|
// function testSHA256() {
|
||||||
|
// const tests = [
|
||||||
|
// {
|
||||||
|
// input: "",
|
||||||
|
// expected:
|
||||||
|
// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// input: "Hello World",
|
||||||
|
// expected:
|
||||||
|
// "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// input: "abc",
|
||||||
|
// expected:
|
||||||
|
// "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// input: "The quick brown fox jumps over the lazy dog",
|
||||||
|
// expected:
|
||||||
|
// "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// console.log("Running SHA-256 tests:");
|
||||||
|
// tests.forEach((test, i) => {
|
||||||
|
// const result = Crypto.sha256(test.input);
|
||||||
|
// const passed = result === test.expected;
|
||||||
|
// console.log(`Test ${i + 1}: ${passed ? "PASS" : "FAIL"}`);
|
||||||
|
// if (!passed) {
|
||||||
|
// console.log(` Input: "${test.input}"`);
|
||||||
|
// console.log(` Expected: ${test.expected}`);
|
||||||
|
// console.log(` Got: ${result}`);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
@@ -7,23 +7,23 @@ import qs.Modules.Misc
|
|||||||
import qs.Modules.Panel
|
import qs.Modules.Panel
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
Scope {
|
ShellRoot {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
Notification {
|
||||||
|
id: notification
|
||||||
|
}
|
||||||
|
|
||||||
IPCService {
|
IPCService {
|
||||||
id: ipcService
|
id: ipcService
|
||||||
}
|
}
|
||||||
|
|
||||||
Bar {
|
Bar {
|
||||||
id: bar
|
id: bar
|
||||||
|
|
||||||
shell: root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Corners {
|
Corners {
|
||||||
id: corners
|
id: corners
|
||||||
|
|
||||||
shell: root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CalendarPanel {
|
CalendarPanel {
|
||||||
@@ -38,4 +38,10 @@ Scope {
|
|||||||
objectName: "controlCenterPanel"
|
objectName: "controlCenterPanel"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationHistoryPanel {
|
||||||
|
id: notificationHistoryPanel
|
||||||
|
|
||||||
|
objectName: "notificationHistoryPanel"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user