Dotfiles
This commit is contained in:
parent
ba54a35a13
commit
adbe4541cb
237 changed files with 64642 additions and 0 deletions
2
alacritty/.config/alacritty/alacritty.toml
Normal file
2
alacritty/.config/alacritty/alacritty.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[colors.normal]
|
||||||
|
black = "#3d3d3d"
|
||||||
1075
emacs/.config/emacs/config.el
Normal file
1075
emacs/.config/emacs/config.el
Normal file
File diff suppressed because it is too large
Load diff
15
emacs/.config/emacs/init.el
Normal file
15
emacs/.config/emacs/init.el
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
;;; -*- lexical-binding: t; -*-
|
||||||
|
(setq custom-file "~/.config/emacs/custom.el")
|
||||||
|
(when (file-exists-p custom-file)
|
||||||
|
(load custom-file))
|
||||||
|
(setq package-user-dir "~/.cache/emacs/elpa")
|
||||||
|
(make-directory package-user-dir t)
|
||||||
|
(require 'package)
|
||||||
|
;;https://github.com/wbolster/emacs-direnv/issues/85
|
||||||
|
(setenv "PATH" (mapconcat 'identity exec-path ":")) ;;fixes direnv losing nix pkgs
|
||||||
|
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
|
||||||
|
(package-initialize)
|
||||||
|
(package-refresh-contents)
|
||||||
|
(add-to-list 'load-path (getenv "EMACSLOADPATH"))
|
||||||
|
|
||||||
|
(load "~/.config/emacs/config.el")
|
||||||
7
git/.config/git/config
Normal file
7
git/.config/git/config
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
[user]
|
||||||
|
name = Aleksandr Lebedev
|
||||||
|
email = alex.lebedev2003@icloud.com
|
||||||
|
[core]
|
||||||
|
editor = emacsclient -c
|
||||||
|
[color]
|
||||||
|
ui = auto
|
||||||
631
niri/.config/niri/config.kdl
Normal file
631
niri/.config/niri/config.kdl
Normal file
|
|
@ -0,0 +1,631 @@
|
||||||
|
// This config is in the KDL format: https://kdl.dev
|
||||||
|
// "/-" comments out the following node.
|
||||||
|
// Check the wiki for a full description of the configuration:
|
||||||
|
// https://yalter.github.io/niri/Configuration:-Introduction
|
||||||
|
|
||||||
|
// Input device configuration.
|
||||||
|
// Find the full list of options on the wiki:
|
||||||
|
// https://yalter.github.io/niri/Configuration:-Input
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
// You can set rules, model, layout, variant and options.
|
||||||
|
// For more information, see xkeyboard-config(7).
|
||||||
|
|
||||||
|
// For example:
|
||||||
|
layout "eu,ru"
|
||||||
|
options "grp:lctrl_toggle, ctrl:nocaps"
|
||||||
|
|
||||||
|
// If this section is empty, niri will fetch xkb settings
|
||||||
|
// from org.freedesktop.locale1. You can control these using
|
||||||
|
// localectl set-x11-keymap.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable numlock on startup, omitting this setting disables it.
|
||||||
|
numlock
|
||||||
|
track-layout "window"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next sections include libinput settings.
|
||||||
|
// Omitting settings disables them, or leaves them at their default values.
|
||||||
|
// All commented-out settings here are examples, not defaults.
|
||||||
|
touchpad {
|
||||||
|
// off
|
||||||
|
tap
|
||||||
|
dwt
|
||||||
|
// dwtp
|
||||||
|
drag true
|
||||||
|
drag-lock
|
||||||
|
natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
// scroll-method "two-finger"
|
||||||
|
// disabled-on-external-mouse
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse {
|
||||||
|
// off
|
||||||
|
// natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
// scroll-method "no-scroll"
|
||||||
|
}
|
||||||
|
|
||||||
|
trackpoint {
|
||||||
|
// off
|
||||||
|
// natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
// scroll-method "on-button-down"
|
||||||
|
// scroll-button 273
|
||||||
|
// scroll-button-lock
|
||||||
|
// middle-emulation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment this to make the mouse warp to the center of newly focused windows.
|
||||||
|
// warp-mouse-to-focus
|
||||||
|
|
||||||
|
// Focus windows and outputs automatically when moving the mouse into them.
|
||||||
|
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
||||||
|
// focus-follows-mouse max-scroll-amount="0%"
|
||||||
|
|
||||||
|
//disable-power-key-handling
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can configure outputs by their name, which you can find
|
||||||
|
// by running `niri msg outputs` while inside a niri instance.
|
||||||
|
// The built-in laptop monitor is usually called "eDP-1".
|
||||||
|
// Find more information on the wiki:
|
||||||
|
// https://yalter.github.io/niri/Configuration:-Outputs
|
||||||
|
// Remember to uncomment the node by removing "/-"!
|
||||||
|
/-output "eDP-1" {
|
||||||
|
// Uncomment this line to disable this output.
|
||||||
|
// off
|
||||||
|
|
||||||
|
// Resolution and, optionally, refresh rate of the output.
|
||||||
|
// The format is "<width>x<height>" or "<width>x<height>@<refresh rate>".
|
||||||
|
// If the refresh rate is omitted, niri will pick the highest refresh rate
|
||||||
|
// for the resolution.
|
||||||
|
// If the mode is omitted altogether or is invalid, niri will pick one automatically.
|
||||||
|
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
|
||||||
|
mode "1920x1080@120.030"
|
||||||
|
|
||||||
|
// You can use integer or fractional scale, for example use 1.5 for 150% scale.
|
||||||
|
scale 2
|
||||||
|
|
||||||
|
// Transform allows to rotate the output counter-clockwise, valid values are:
|
||||||
|
// normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270.
|
||||||
|
transform "normal"
|
||||||
|
|
||||||
|
// Position of the output in the global coordinate space.
|
||||||
|
// This affects directional monitor actions like "focus-monitor-left", and cursor movement.
|
||||||
|
// The cursor can only move between directly adjacent outputs.
|
||||||
|
// Output scale and rotation has to be taken into account for positioning:
|
||||||
|
// outputs are sized in logical, or scaled, pixels.
|
||||||
|
// For example, a 3840×2160 output with scale 2.0 will have a logical size of 1920×1080,
|
||||||
|
// so to put another output directly adjacent to it on the right, set its x to 1920.
|
||||||
|
// If the position is unset or results in an overlap, the output is instead placed
|
||||||
|
// automatically.
|
||||||
|
position x=1280 y=0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings that influence how windows are positioned and sized.
|
||||||
|
// Find more information on the wiki:
|
||||||
|
// https://yalter.github.io/niri/Configuration:-Layout
|
||||||
|
layout {
|
||||||
|
// Set gaps around windows in logical pixels.
|
||||||
|
gaps 16
|
||||||
|
|
||||||
|
// When to center a column when changing focus, options are:
|
||||||
|
// - "never", default behavior, focusing an off-screen column will keep at the left
|
||||||
|
// or right edge of the screen.
|
||||||
|
// - "always", the focused column will always be centered.
|
||||||
|
// - "on-overflow", focusing a column will center it if it doesn't fit
|
||||||
|
// together with the previously focused column.
|
||||||
|
center-focused-column "never"
|
||||||
|
|
||||||
|
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
|
||||||
|
preset-column-widths {
|
||||||
|
// Proportion sets the width as a fraction of the output width, taking gaps into account.
|
||||||
|
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
|
||||||
|
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
|
||||||
|
proportion 1.0
|
||||||
|
proportion 0.5
|
||||||
|
proportion 0.33333
|
||||||
|
proportion 0.66667
|
||||||
|
|
||||||
|
// Fixed sets the width in logical pixels exactly.
|
||||||
|
// fixed 1920
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
|
||||||
|
// preset-window-heights { }
|
||||||
|
|
||||||
|
// You can change the default width of the new windows.
|
||||||
|
default-column-width { proportion 1.0; }
|
||||||
|
// If you leave the brackets empty, the windows themselves will decide their initial width.
|
||||||
|
// default-column-width {}
|
||||||
|
|
||||||
|
// By default focus ring and border are rendered as a solid background rectangle
|
||||||
|
// behind windows. That is, they will show up through semitransparent windows.
|
||||||
|
// This is because windows using client-side decorations can have an arbitrary shape.
|
||||||
|
//
|
||||||
|
// If you don't like that, you should uncomment `prefer-no-csd` below.
|
||||||
|
// Niri will draw focus ring and border *around* windows that agree to omit their
|
||||||
|
// client-side decorations.
|
||||||
|
//
|
||||||
|
// Alternatively, you can override it with a window rule called
|
||||||
|
// `draw-border-with-background`.
|
||||||
|
|
||||||
|
// You can change how the focus ring looks.
|
||||||
|
focus-ring {
|
||||||
|
// Uncomment this line to disable the focus ring.
|
||||||
|
// off
|
||||||
|
|
||||||
|
// How many logical pixels the ring extends out from the windows.
|
||||||
|
width 4
|
||||||
|
|
||||||
|
// Colors can be set in a variety of ways:
|
||||||
|
// - CSS named colors: "red"
|
||||||
|
// - RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa"
|
||||||
|
// - CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others.
|
||||||
|
|
||||||
|
// Color of the ring on the active monitor.
|
||||||
|
active-color "#b19cd9" //Light Pastel Purple
|
||||||
|
|
||||||
|
// Color of the ring on inactive monitors.
|
||||||
|
//
|
||||||
|
// The focus ring only draws around the active window, so the only place
|
||||||
|
// where you can see its inactive-color is on other monitors.
|
||||||
|
inactive-color "#e6e6fa" //Lavender Mist
|
||||||
|
|
||||||
|
// You can also use gradients. They take precedence over solid colors.
|
||||||
|
// Gradients are rendered the same as CSS linear-gradient(angle, from, to).
|
||||||
|
// The angle is the same as in linear-gradient, and is optional,
|
||||||
|
// defaulting to 180 (top-to-bottom gradient).
|
||||||
|
// You can use any CSS linear-gradient tool on the web to set these up.
|
||||||
|
// Changing the color space is also supported, check the wiki for more info.
|
||||||
|
//
|
||||||
|
// active-gradient from="#80c8ff" to="#c7ff7f" angle=45
|
||||||
|
|
||||||
|
// You can also color the gradient relative to the entire view
|
||||||
|
// of the workspace, rather than relative to just the window itself.
|
||||||
|
// To do that, set relative-to="workspace-view".
|
||||||
|
//
|
||||||
|
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can also add a border. It's similar to the focus ring, but always visible.
|
||||||
|
border {
|
||||||
|
// The settings are the same as for the focus ring.
|
||||||
|
// If you enable the border, you probably want to disable the focus ring.
|
||||||
|
off
|
||||||
|
|
||||||
|
width 4
|
||||||
|
active-color "#ffc87f"
|
||||||
|
inactive-color "#505050"
|
||||||
|
|
||||||
|
// Color of the border around windows that request your attention.
|
||||||
|
urgent-color "#9b0000"
|
||||||
|
|
||||||
|
// Gradients can use a few different interpolation color spaces.
|
||||||
|
// For example, this is a pastel rainbow gradient via in="oklch longer hue".
|
||||||
|
//
|
||||||
|
// active-gradient from="#e5989b" to="#ffb4a2" angle=45 relative-to="workspace-view" in="oklch longer hue"
|
||||||
|
|
||||||
|
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can enable drop shadows for windows.
|
||||||
|
shadow {
|
||||||
|
// Uncomment the next line to enable shadows.
|
||||||
|
// on
|
||||||
|
|
||||||
|
// By default, the shadow draws only around its window, and not behind it.
|
||||||
|
// Uncomment this setting to make the shadow draw behind its window.
|
||||||
|
//
|
||||||
|
// Note that niri has no way of knowing about the CSD window corner
|
||||||
|
// radius. It has to assume that windows have square corners, leading to
|
||||||
|
// shadow artifacts inside the CSD rounded corners. This setting fixes
|
||||||
|
// those artifacts.
|
||||||
|
//
|
||||||
|
// However, instead you may want to set prefer-no-csd and/or
|
||||||
|
// geometry-corner-radius. Then, niri will know the corner radius and
|
||||||
|
// draw the shadow correctly, without having to draw it behind the
|
||||||
|
// window. These will also remove client-side shadows if the window
|
||||||
|
// draws any.
|
||||||
|
//
|
||||||
|
// draw-behind-window true
|
||||||
|
|
||||||
|
// You can change how shadows look. The values below are in logical
|
||||||
|
// pixels and match the CSS box-shadow properties.
|
||||||
|
|
||||||
|
// Softness controls the shadow blur radius.
|
||||||
|
softness 30
|
||||||
|
|
||||||
|
// Spread expands the shadow.
|
||||||
|
spread 5
|
||||||
|
|
||||||
|
// Offset moves the shadow relative to the window.
|
||||||
|
offset x=0 y=5
|
||||||
|
|
||||||
|
// You can also change the shadow color and opacity.
|
||||||
|
color "#0007"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
|
||||||
|
// You can think of them as a kind of outer gaps. They are set in logical pixels.
|
||||||
|
// Left and right struts will cause the next window to the side to always be visible.
|
||||||
|
// Top and bottom struts will simply add outer gaps in addition to the area occupied by
|
||||||
|
// layer-shell panels and regular gaps.
|
||||||
|
struts {
|
||||||
|
// left 64
|
||||||
|
// right 64
|
||||||
|
// top 64
|
||||||
|
// bottom 64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add lines like this to spawn processes at startup.
|
||||||
|
// Note that running niri as a session supports xdg-desktop-autostart,
|
||||||
|
// which may be more convenient to use.
|
||||||
|
// See the binds section below for more spawn examples.
|
||||||
|
|
||||||
|
spawn-sh-at-startup "/usr/bin/pipewire-launcher.sh"
|
||||||
|
spawn-sh-at-startup "gnome-keyring-daemon"
|
||||||
|
spawn-sh-at-startup "alacritty --daemon"
|
||||||
|
spawn-sh-at-startup "emacs --daemon"
|
||||||
|
spawn-sh-at-startup "qs"
|
||||||
|
spawn-sh-at-startup "wl-paste --watch cliphist store &"
|
||||||
|
spawn-sh-at-startup "nextcloud --background"
|
||||||
|
|
||||||
|
config-notification {
|
||||||
|
disable-failed
|
||||||
|
}
|
||||||
|
|
||||||
|
debug {
|
||||||
|
honor-xdg-activation-with-invalid-serial
|
||||||
|
}
|
||||||
|
|
||||||
|
hotkey-overlay {
|
||||||
|
// Uncomment this line to disable the "Important Hotkeys" pop-up at startup.
|
||||||
|
// skip-at-startup
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
|
||||||
|
// If the client will specifically ask for CSD, the request will be honored.
|
||||||
|
// Additionally, clients will be informed that they are tiled, removing some client-side rounded corners.
|
||||||
|
// This option will also fix border/focus ring drawing behind some semitransparent windows.
|
||||||
|
// After enabling or disabling this, you need to restart the apps for this to take effect.
|
||||||
|
prefer-no-csd
|
||||||
|
|
||||||
|
// You can change the path where screenshots are saved.
|
||||||
|
// A ~ at the front will be expanded to the home directory.
|
||||||
|
// The path is formatted with strftime(3) to give you the screenshot date and time.
|
||||||
|
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
|
||||||
|
|
||||||
|
// You can also set this to null to disable saving screenshots to disk.
|
||||||
|
// screenshot-path null
|
||||||
|
|
||||||
|
// Animation settings.
|
||||||
|
// The wiki explains how to configure individual animations:
|
||||||
|
// https://yalter.github.io/niri/Configuration:-Animations
|
||||||
|
animations {
|
||||||
|
// Uncomment to turn off all animations.
|
||||||
|
// off
|
||||||
|
|
||||||
|
// Slow down all animations by this factor. Values below 1 speed them up instead.
|
||||||
|
// slowdown 3.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window rules let you adjust behavior for individual windows.
|
||||||
|
// Find more information on the wiki:
|
||||||
|
// https://yalter.github.io/niri/Configuration:-Window-Rules
|
||||||
|
|
||||||
|
// Work around WezTerm's initial configure bug
|
||||||
|
// by setting an empty default-column-width.
|
||||||
|
window-rule {
|
||||||
|
// This regular expression is intentionally made as specific as possible,
|
||||||
|
// since this is the default config, and we want no false positives.
|
||||||
|
// You can get away with just app-id="wezterm" if you want.
|
||||||
|
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
||||||
|
default-column-width {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the Firefox picture-in-picture player as floating by default.
|
||||||
|
window-rule {
|
||||||
|
// This app-id regular expression will work for both:
|
||||||
|
// - host Firefox (app-id is "firefox")
|
||||||
|
// - Flatpak Firefox (app-id is "org.mozilla.firefox")
|
||||||
|
match app-id=r#"librewolf$"# title="^Picture-in-Picture$"
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: block out two password managers from screen capture.
|
||||||
|
// (This example rule is commented out with a "/-" in front.)
|
||||||
|
/-window-rule {
|
||||||
|
match app-id=r#"^org\.keepassxc\.KeePassXC$"#
|
||||||
|
match app-id=r#"^org\.gnome\.World\.Secrets$"#
|
||||||
|
|
||||||
|
block-out-from "screen-capture"
|
||||||
|
|
||||||
|
// Use this instead if you want them visible on third-party screenshot tools.
|
||||||
|
// block-out-from "screencast"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: enable rounded corners for all windows.
|
||||||
|
// (This example rule is commented out with a "/-" in front.)
|
||||||
|
window-rule {
|
||||||
|
geometry-corner-radius 12
|
||||||
|
clip-to-geometry true
|
||||||
|
}
|
||||||
|
|
||||||
|
binds {
|
||||||
|
// Keys consist of modifiers separated by + signs, followed by an XKB key name
|
||||||
|
// in the end. To find an XKB name for a particular key, you may use a program
|
||||||
|
// like wev.
|
||||||
|
//
|
||||||
|
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
|
||||||
|
// when running as a winit window.
|
||||||
|
//
|
||||||
|
// Most actions that you can bind here can also be invoked programmatically with
|
||||||
|
// `niri msg action do-something`.
|
||||||
|
|
||||||
|
// Mod-Shift-/, which is usually the same as Mod-?,
|
||||||
|
// shows a list of important hotkeys.
|
||||||
|
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||||
|
|
||||||
|
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
||||||
|
Mod+T hotkey-overlay-title="Open a Terminal: Alacritty" { spawn-sh "alacritty msg create-window"; }
|
||||||
|
Mod+E hotkey-overlay-title="Run Emacs" { spawn-sh "emacsclient -c"; }
|
||||||
|
Mod+B hotkey-overlay-title="Open a browser: Librewolf" { spawn "librewolf"; }
|
||||||
|
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
|
||||||
|
|
||||||
|
// Use spawn-sh to run a shell command. Do this if you need pipes, multiple commands, etc.
|
||||||
|
Mod+Space hotkey-overlay-title="Application Launcher" {
|
||||||
|
spawn-sh "qs ipc call spotlight toggle";
|
||||||
|
}
|
||||||
|
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
||||||
|
spawn-sh "qs ipc call clipboard toggle";
|
||||||
|
}
|
||||||
|
Mod+M hotkey-overlay-title="Task Manager" {
|
||||||
|
spawn-sh "qs ipc call processlist toggle";
|
||||||
|
}
|
||||||
|
Super+L hotkey-overlay-title="Lock Screen" {
|
||||||
|
spawn-sh "qs ipc call lock lock";
|
||||||
|
}
|
||||||
|
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
||||||
|
spawn-sh "qs ipc call dankdash wallpaper";
|
||||||
|
}
|
||||||
|
XF86AudioRaiseVolume allow-when-locked=true {
|
||||||
|
spawn-sh "qs ipc call audio increment 3";
|
||||||
|
}
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true {
|
||||||
|
spawn-sh "qs ipc call audio decrement 3";
|
||||||
|
}
|
||||||
|
XF86AudioMute allow-when-locked=true {
|
||||||
|
spawn-sh "qs ipc call audio mute";
|
||||||
|
}
|
||||||
|
XF86AudioMicMute allow-when-locked=true {
|
||||||
|
spawn-sh "qs ipc call audio micmute";
|
||||||
|
}
|
||||||
|
XF86MonBrightnessUp allow-when-locked=true {
|
||||||
|
spawn-sh "qs ipc call brightness increment 5 ''";
|
||||||
|
}
|
||||||
|
// You can override the default device for e.g. keyboards by adding the device name to the last param
|
||||||
|
XF86MonBrightnessDown allow-when-locked=true {
|
||||||
|
spawn-sh "qs ipc call brightness decrement 5 ''";
|
||||||
|
}
|
||||||
|
// Night mode toggle
|
||||||
|
Mod+Shift+N allow-when-locked=true {
|
||||||
|
spawn-sh "qs ipc call night toggle";
|
||||||
|
}
|
||||||
|
// Open/close the Overview: a zoomed-out view of workspaces and windows.
|
||||||
|
// You can also move the mouse into the top-left hot corner,
|
||||||
|
// or do a four-finger swipe up on a touchpad.
|
||||||
|
Mod+Tab repeat=false { toggle-overview; }
|
||||||
|
|
||||||
|
Mod+Q repeat=false { close-window; }
|
||||||
|
|
||||||
|
Mod+Left { focus-column-left; }
|
||||||
|
Mod+Down { focus-window-or-workspace-down; }
|
||||||
|
Mod+Up { focus-window-or-workspace-up; }
|
||||||
|
Mod+Right { focus-column-right; }
|
||||||
|
|
||||||
|
Mod+Shift+Left { move-column-left; }
|
||||||
|
Mod+Shift+Down { move-window-down-or-to-workspace-down; }
|
||||||
|
Mod+Shift+Up { move-window-up-or-to-workspace-up; }
|
||||||
|
Mod+Shift+Right { move-column-right; }
|
||||||
|
|
||||||
|
Mod+Home { focus-column-first; }
|
||||||
|
Mod+End { focus-column-last; }
|
||||||
|
Mod+Ctrl+Home { move-column-to-first; }
|
||||||
|
Mod+Ctrl+End { move-column-to-last; }
|
||||||
|
|
||||||
|
Mod+Ctrl+Left { focus-monitor-left; }
|
||||||
|
Mod+Ctrl+Down { focus-monitor-down; }
|
||||||
|
Mod+Ctrl+Up { focus-monitor-up; }
|
||||||
|
Mod+Ctrl+Right { focus-monitor-right; }
|
||||||
|
|
||||||
|
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
|
||||||
|
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
|
||||||
|
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
|
||||||
|
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
|
||||||
|
|
||||||
|
// Alternatively, there are commands to move just a single window:
|
||||||
|
// Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// And you can also move a whole workspace to another monitor:
|
||||||
|
// Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; }
|
||||||
|
// ...
|
||||||
|
|
||||||
|
Mod+Page_Down { focus-workspace-down; }
|
||||||
|
Mod+Page_Up { focus-workspace-up; }
|
||||||
|
Mod+U { focus-workspace-down; }
|
||||||
|
Mod+I { focus-workspace-up; }
|
||||||
|
Mod+Ctrl+Page_Down { move-column-to-workspace-down; }
|
||||||
|
Mod+Ctrl+Page_Up { move-column-to-workspace-up; }
|
||||||
|
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||||
|
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
// Alternatively, there are commands to move just a single window:
|
||||||
|
// Mod+Ctrl+Page_Down { move-window-to-workspace-down; }
|
||||||
|
// ...
|
||||||
|
|
||||||
|
Mod+Shift+Page_Down { move-workspace-down; }
|
||||||
|
Mod+Shift+Page_Up { move-workspace-up; }
|
||||||
|
Mod+Shift+U { move-workspace-down; }
|
||||||
|
Mod+Shift+I { move-workspace-up; }
|
||||||
|
|
||||||
|
// You can bind mouse wheel scroll ticks using the following syntax.
|
||||||
|
// These binds will change direction based on the natural-scroll setting.
|
||||||
|
//
|
||||||
|
// To avoid scrolling through workspaces really fast, you can use
|
||||||
|
// the cooldown-ms property. The bind will be rate-limited to this value.
|
||||||
|
// You can set a cooldown on any bind, but it's most useful for the wheel.
|
||||||
|
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
||||||
|
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
|
||||||
|
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
|
||||||
|
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
Mod+WheelScrollRight { focus-column-right; }
|
||||||
|
Mod+WheelScrollLeft { focus-column-left; }
|
||||||
|
Mod+Ctrl+WheelScrollRight { move-column-right; }
|
||||||
|
Mod+Ctrl+WheelScrollLeft { move-column-left; }
|
||||||
|
|
||||||
|
// Usually scrolling up and down with Shift in applications results in
|
||||||
|
// horizontal scrolling; these binds replicate that.
|
||||||
|
Mod+Shift+WheelScrollDown { focus-column-right; }
|
||||||
|
Mod+Shift+WheelScrollUp { focus-column-left; }
|
||||||
|
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
|
||||||
|
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
|
||||||
|
|
||||||
|
// Similarly, you can bind touchpad scroll "ticks".
|
||||||
|
// Touchpad scrolling is continuous, so for these binds it is split into
|
||||||
|
// discrete intervals.
|
||||||
|
// These binds are also affected by touchpad's natural-scroll, so these
|
||||||
|
// example binds are "inverted", since we have natural-scroll enabled for
|
||||||
|
// touchpads by default.
|
||||||
|
// Mod+TouchpadScrollDown { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02+"; }
|
||||||
|
// Mod+TouchpadScrollUp { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02-"; }
|
||||||
|
|
||||||
|
// You can refer to workspaces by index. However, keep in mind that
|
||||||
|
// niri is a dynamic workspace system, so these commands are kind of
|
||||||
|
// "best effort". Trying to refer to a workspace index bigger than
|
||||||
|
// the current workspace count will instead refer to the bottommost
|
||||||
|
// (empty) workspace.
|
||||||
|
//
|
||||||
|
// For example, with 2 workspaces + 1 empty, indices 3, 4, 5 and so on
|
||||||
|
// will all refer to the 3rd workspace.
|
||||||
|
Mod+1 { focus-workspace 1; }
|
||||||
|
Mod+2 { focus-workspace 2; }
|
||||||
|
Mod+3 { focus-workspace 3; }
|
||||||
|
Mod+4 { focus-workspace 4; }
|
||||||
|
Mod+5 { focus-workspace 5; }
|
||||||
|
Mod+6 { focus-workspace 6; }
|
||||||
|
Mod+7 { focus-workspace 7; }
|
||||||
|
Mod+8 { focus-workspace 8; }
|
||||||
|
Mod+9 { focus-workspace 9; }
|
||||||
|
Mod+Ctrl+1 { move-column-to-workspace 1; }
|
||||||
|
Mod+Ctrl+2 { move-column-to-workspace 2; }
|
||||||
|
Mod+Ctrl+3 { move-column-to-workspace 3; }
|
||||||
|
Mod+Ctrl+4 { move-column-to-workspace 4; }
|
||||||
|
Mod+Ctrl+5 { move-column-to-workspace 5; }
|
||||||
|
Mod+Ctrl+6 { move-column-to-workspace 6; }
|
||||||
|
Mod+Ctrl+7 { move-column-to-workspace 7; }
|
||||||
|
Mod+Ctrl+8 { move-column-to-workspace 8; }
|
||||||
|
Mod+Ctrl+9 { move-column-to-workspace 9; }
|
||||||
|
|
||||||
|
// Alternatively, there are commands to move just a single window:
|
||||||
|
// Mod+Ctrl+1 { move-window-to-workspace 1; }
|
||||||
|
|
||||||
|
// Switches focus between the current and the previous workspace.
|
||||||
|
// Mod+Tab { focus-workspace-previous; }
|
||||||
|
|
||||||
|
// The following binds move the focused window in and out of a column.
|
||||||
|
// If the window is alone, they will consume it into the nearby column to the side.
|
||||||
|
// If the window is already in a column, they will expel it out.
|
||||||
|
Mod+BracketLeft { consume-or-expel-window-left; }
|
||||||
|
Mod+BracketRight { consume-or-expel-window-right; }
|
||||||
|
|
||||||
|
// Consume one window from the right to the bottom of the focused column.
|
||||||
|
Mod+Comma { consume-window-into-column; }
|
||||||
|
// Expel the bottom window from the focused column to the right.
|
||||||
|
Mod+Period { expel-window-from-column; }
|
||||||
|
|
||||||
|
Mod+R { switch-preset-column-width; }
|
||||||
|
// Cycling through the presets in reverse order is also possible.
|
||||||
|
// Mod+R { switch-preset-column-width-back; }
|
||||||
|
Mod+Shift+R { switch-preset-window-height; }
|
||||||
|
Mod+Ctrl+R { reset-window-height; }
|
||||||
|
Mod+Shift+F { maximize-column; }
|
||||||
|
Mod+F { fullscreen-window; }
|
||||||
|
|
||||||
|
// Expand the focused column to space not taken up by other fully visible columns.
|
||||||
|
// Makes the column "fill the rest of the space".
|
||||||
|
Mod+Ctrl+F { expand-column-to-available-width; }
|
||||||
|
|
||||||
|
Mod+C { center-column; }
|
||||||
|
|
||||||
|
// Center all fully visible columns on screen.
|
||||||
|
Mod+Ctrl+C { center-visible-columns; }
|
||||||
|
|
||||||
|
// Finer width adjustments.
|
||||||
|
// This command can also:
|
||||||
|
// * set width in pixels: "1000"
|
||||||
|
// * adjust width in pixels: "-5" or "+5"
|
||||||
|
// * set width as a percentage of screen width: "25%"
|
||||||
|
// * adjust width as a percentage of screen width: "-10%" or "+10%"
|
||||||
|
// Pixel sizes use logical, or scaled, pixels. I.e. on an output with scale 2.0,
|
||||||
|
// set-column-width "100" will make the column occupy 200 physical screen pixels.
|
||||||
|
Mod+Minus { set-column-width "-10%"; }
|
||||||
|
Mod+Equal { set-column-width "+10%"; }
|
||||||
|
|
||||||
|
// Finer height adjustments when in column with other windows.
|
||||||
|
Mod+Shift+Minus { set-window-height "-10%"; }
|
||||||
|
Mod+Shift+Equal { set-window-height "+10%"; }
|
||||||
|
|
||||||
|
// Move the focused window between the floating and the tiling layout.
|
||||||
|
Mod+Shift+V { toggle-window-floating; }
|
||||||
|
Mod+Ctrl+V { switch-focus-between-floating-and-tiling; }
|
||||||
|
|
||||||
|
// Toggle tabbed column display mode.
|
||||||
|
// Windows in this column will appear as vertical tabs,
|
||||||
|
// rather than stacked on top of each other.
|
||||||
|
Mod+W { toggle-column-tabbed-display; }
|
||||||
|
|
||||||
|
// Actions to switch layouts.
|
||||||
|
// Note: if you uncomment these, make sure you do NOT have
|
||||||
|
// a matching layout switch hotkey configured in xkb options above.
|
||||||
|
// Having both at once on the same hotkey will break the switching,
|
||||||
|
// since it will switch twice upon pressing the hotkey (once by xkb, once by niri).
|
||||||
|
// Mod+Space { switch-layout "next"; }
|
||||||
|
// Mod+Shift+Space { switch-layout "prev"; }
|
||||||
|
|
||||||
|
Print { screenshot; }
|
||||||
|
Ctrl+Print { screenshot-screen; }
|
||||||
|
Alt+Print { screenshot-window; }
|
||||||
|
|
||||||
|
// Applications such as remote-desktop clients and software KVM switches may
|
||||||
|
// request that niri stops processing the keyboard shortcuts defined here
|
||||||
|
// so they may, for example, forward the key presses as-is to a remote machine.
|
||||||
|
// It's a good idea to bind an escape hatch to toggle the inhibitor,
|
||||||
|
// so a buggy application can't hold your session hostage.
|
||||||
|
//
|
||||||
|
// The allow-inhibiting=false property can be applied to other binds as well,
|
||||||
|
// which ensures niri always processes them, even when an inhibitor is active.
|
||||||
|
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
||||||
|
|
||||||
|
// The quit action will show a confirmation dialog to avoid accidental exits.
|
||||||
|
Mod+Shift+E { quit; }
|
||||||
|
Ctrl+Alt+Delete { quit; }
|
||||||
|
|
||||||
|
// Powers off the monitors. To turn them back on, do any input like
|
||||||
|
// moving the mouse or pressing any other key.
|
||||||
|
Mod+Shift+P { power-off-monitors; }
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor {
|
||||||
|
hide-after-inactive-ms 10000
|
||||||
|
}
|
||||||
99
quickshell/.config/quickshell/.gitignore
vendored
Normal file
99
quickshell/.config/quickshell/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
# C++ objects and libs
|
||||||
|
*.slo
|
||||||
|
*.lo
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.la
|
||||||
|
*.lai
|
||||||
|
*.so
|
||||||
|
*.so.*
|
||||||
|
*.dll
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Qt-es
|
||||||
|
object_script.*.Release
|
||||||
|
object_script.*.Debug
|
||||||
|
*_plugin_import.cpp
|
||||||
|
/.qmake.cache
|
||||||
|
/.qmake.stash
|
||||||
|
*.pro.user
|
||||||
|
*.pro.user.*
|
||||||
|
*.qbs.user
|
||||||
|
*.qbs.user.*
|
||||||
|
*.moc
|
||||||
|
moc_*.cpp
|
||||||
|
moc_*.h
|
||||||
|
qrc_*.cpp
|
||||||
|
ui_*.h
|
||||||
|
*.qmlc
|
||||||
|
*.jsc
|
||||||
|
Makefile*
|
||||||
|
*build-*
|
||||||
|
*.qm
|
||||||
|
*.prl
|
||||||
|
|
||||||
|
# Qt unit tests
|
||||||
|
target_wrapper.*
|
||||||
|
|
||||||
|
# QtCreator
|
||||||
|
*.autosave
|
||||||
|
|
||||||
|
# QtCreator Qml
|
||||||
|
*.qmlproject.user
|
||||||
|
*.qmlproject.user.*
|
||||||
|
|
||||||
|
# QtCreator CMake
|
||||||
|
CMakeLists.txt.user*
|
||||||
|
|
||||||
|
# QtCreator 4.8< compilation database
|
||||||
|
compile_commands.json
|
||||||
|
|
||||||
|
# QtCreator local machine specific files for imported projects
|
||||||
|
*creator.user*
|
||||||
|
|
||||||
|
*_qmlcache.qrc
|
||||||
|
UNUSED
|
||||||
|
.qmlls.ini
|
||||||
|
|
||||||
|
CLAUDE-activeContext.md
|
||||||
|
CLAUDE-temp.md
|
||||||
|
|
||||||
|
# Auto-generated theme files
|
||||||
|
*.generated.*
|
||||||
|
niri-colors.generated.kdl
|
||||||
|
ghostty-colors.generated.conf
|
||||||
|
|
||||||
|
result
|
||||||
|
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Code coverage profiles and other test artifacts
|
||||||
|
*.out
|
||||||
|
coverage.*
|
||||||
|
*.coverprofile
|
||||||
|
profile.cov
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Editor/IDE
|
||||||
|
# .idea/
|
||||||
|
# .vscode/
|
||||||
25
quickshell/.config/quickshell/Common/Anims.qml
Normal file
25
quickshell/.config/quickshell/Common/Anims.qml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property int durShort: 200
|
||||||
|
readonly property int durMed: 450
|
||||||
|
readonly property int durLong: 600
|
||||||
|
|
||||||
|
readonly property int slidePx: 80
|
||||||
|
|
||||||
|
readonly property var emphasized: [0.05, 0.00, 0.133333, 0.06, 0.166667, 0.40, 0.208333, 0.82, 0.25, 1.00, 1.00, 1.00]
|
||||||
|
|
||||||
|
readonly property var emphasizedDecel: [0.05, 0.70, 0.10, 1.00, 1.00, 1.00]
|
||||||
|
|
||||||
|
readonly property var emphasizedAccel: [0.30, 0.00, 0.80, 0.15, 1.00, 1.00]
|
||||||
|
|
||||||
|
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
|
||||||
|
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
|
||||||
|
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
|
||||||
|
}
|
||||||
131
quickshell/.config/quickshell/Common/AppUsageHistoryData.qml
Normal file
131
quickshell/.config/quickshell/Common/AppUsageHistoryData.qml
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var appUsageRanking: {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
loadSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
parseSettings(settingsFile.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSettings(content) {
|
||||||
|
try {
|
||||||
|
if (content && content.trim()) {
|
||||||
|
var settings = JSON.parse(content)
|
||||||
|
appUsageRanking = settings.appUsageRanking || {}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
settingsFile.setText(JSON.stringify({
|
||||||
|
"appUsageRanking": appUsageRanking
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAppUsage(app) {
|
||||||
|
if (!app)
|
||||||
|
return
|
||||||
|
|
||||||
|
var appId = app.id || (app.execString || app.exec || "")
|
||||||
|
if (!appId)
|
||||||
|
return
|
||||||
|
|
||||||
|
var currentRanking = Object.assign({}, appUsageRanking)
|
||||||
|
|
||||||
|
if (currentRanking[appId]) {
|
||||||
|
currentRanking[appId].usageCount = (currentRanking[appId].usageCount
|
||||||
|
|| 1) + 1
|
||||||
|
currentRanking[appId].lastUsed = Date.now()
|
||||||
|
currentRanking[appId].icon = app.icon || currentRanking[appId].icon
|
||||||
|
|| "application-x-executable"
|
||||||
|
currentRanking[appId].name = app.name
|
||||||
|
|| currentRanking[appId].name || ""
|
||||||
|
} else {
|
||||||
|
currentRanking[appId] = {
|
||||||
|
"name": app.name || "",
|
||||||
|
"exec": app.execString || app.exec || "",
|
||||||
|
"icon": app.icon || "application-x-executable",
|
||||||
|
"comment": app.comment || "",
|
||||||
|
"usageCount": 1,
|
||||||
|
"lastUsed": Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appUsageRanking = currentRanking
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppUsageRanking() {
|
||||||
|
return appUsageRanking
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRankedApps() {
|
||||||
|
var apps = []
|
||||||
|
for (var appId in appUsageRanking) {
|
||||||
|
var appData = appUsageRanking[appId]
|
||||||
|
apps.push({
|
||||||
|
"id": appId,
|
||||||
|
"name": appData.name,
|
||||||
|
"exec": appData.exec,
|
||||||
|
"icon": appData.icon,
|
||||||
|
"comment": appData.comment,
|
||||||
|
"usageCount": appData.usageCount,
|
||||||
|
"lastUsed": appData.lastUsed
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps.sort(function (a, b) {
|
||||||
|
if (a.usageCount !== b.usageCount)
|
||||||
|
return b.usageCount - a.usageCount
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupAppUsageRanking(availableAppIds) {
|
||||||
|
var currentRanking = Object.assign({}, appUsageRanking)
|
||||||
|
var hasChanges = false
|
||||||
|
|
||||||
|
for (var appId in currentRanking) {
|
||||||
|
if (availableAppIds.indexOf(appId) === -1) {
|
||||||
|
delete currentRanking[appId]
|
||||||
|
hasChanges = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
appUsageRanking = currentRanking
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: settingsFile
|
||||||
|
|
||||||
|
path: StandardPaths.writableLocation(
|
||||||
|
StandardPaths.GenericStateLocation) + "/DankMaterialShell/appusage.json"
|
||||||
|
blockLoading: true
|
||||||
|
blockWrites: true
|
||||||
|
watchChanges: true
|
||||||
|
onLoaded: {
|
||||||
|
parseSettings(settingsFile.text())
|
||||||
|
}
|
||||||
|
onLoadFailed: error => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
quickshell/.config/quickshell/Common/Appearance.qml
Normal file
66
quickshell/.config/quickshell/Common/Appearance.qml
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property Rounding rounding: Rounding {}
|
||||||
|
readonly property Spacing spacing: Spacing {}
|
||||||
|
readonly property FontSize fontSize: FontSize {}
|
||||||
|
readonly property Anim anim: Anim {}
|
||||||
|
|
||||||
|
component Rounding: QtObject {
|
||||||
|
readonly property int small: 8
|
||||||
|
readonly property int normal: 12
|
||||||
|
readonly property int large: 16
|
||||||
|
readonly property int extraLarge: 24
|
||||||
|
readonly property int full: 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
component Spacing: QtObject {
|
||||||
|
readonly property int small: 4
|
||||||
|
readonly property int normal: 8
|
||||||
|
readonly property int large: 12
|
||||||
|
readonly property int extraLarge: 16
|
||||||
|
readonly property int huge: 24
|
||||||
|
}
|
||||||
|
|
||||||
|
component FontSize: QtObject {
|
||||||
|
readonly property int small: 12
|
||||||
|
readonly property int normal: 14
|
||||||
|
readonly property int large: 16
|
||||||
|
readonly property int extraLarge: 20
|
||||||
|
readonly property int huge: 24
|
||||||
|
}
|
||||||
|
|
||||||
|
component AnimCurves: QtObject {
|
||||||
|
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
|
||||||
|
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
|
||||||
|
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
|
||||||
|
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1
|
||||||
|
/ 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
|
||||||
|
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
|
||||||
|
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
|
||||||
|
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
|
||||||
|
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
||||||
|
readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
component AnimDurations: QtObject {
|
||||||
|
readonly property int quick: 150
|
||||||
|
readonly property int normal: 300
|
||||||
|
readonly property int slow: 500
|
||||||
|
readonly property int extraSlow: 1000
|
||||||
|
readonly property int expressiveFastSpatial: 350
|
||||||
|
readonly property int expressiveDefaultSpatial: 500
|
||||||
|
readonly property int expressiveEffects: 200
|
||||||
|
}
|
||||||
|
|
||||||
|
component Anim: QtObject {
|
||||||
|
readonly property AnimCurves curves: AnimCurves {}
|
||||||
|
readonly property AnimDurations durations: AnimDurations {}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
quickshell/.config/quickshell/Common/CacheUtils.qml
Normal file
45
quickshell/.config/quickshell/Common/CacheUtils.qml
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import Quickshell
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Clear all image cache
|
||||||
|
function clearImageCache() {
|
||||||
|
Quickshell.execDetached(["rm", "-rf", Paths.stringify(
|
||||||
|
Paths.imagecache)])
|
||||||
|
Paths.mkdir(Paths.imagecache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cache older than specified minutes
|
||||||
|
function clearOldCache(ageInMinutes) {
|
||||||
|
Quickshell.execDetached(
|
||||||
|
["find", Paths.stringify(
|
||||||
|
Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cache for specific size
|
||||||
|
function clearCacheForSize(size) {
|
||||||
|
Quickshell.execDetached(
|
||||||
|
["find", Paths.stringify(
|
||||||
|
Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cache size in MB
|
||||||
|
function getCacheSize(callback) {
|
||||||
|
var process = Qt.createQmlObject(`
|
||||||
|
import Quickshell.Io
|
||||||
|
Process {
|
||||||
|
command: ["du", "-sm", "${Paths.stringify(
|
||||||
|
Paths.imagecache)}"]
|
||||||
|
running: true
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
var sizeMB = parseInt(text.split("\\t")[0]) || 0
|
||||||
|
callback(sizeMB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, root)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
quickshell/.config/quickshell/Common/ModalManager.qml
Normal file
16
quickshell/.config/quickshell/Common/ModalManager.qml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: modalManager
|
||||||
|
|
||||||
|
signal closeAllModalsExcept(var excludedModal)
|
||||||
|
|
||||||
|
function openModal(modal) {
|
||||||
|
if (!modal.allowStacking) {
|
||||||
|
closeAllModalsExcept(modal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
quickshell/.config/quickshell/Common/Paths.qml
Normal file
61
quickshell/.config/quickshell/Common/Paths.qml
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtCore
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property url home: StandardPaths.standardLocations(
|
||||||
|
StandardPaths.HomeLocation)[0]
|
||||||
|
readonly property url pictures: StandardPaths.standardLocations(
|
||||||
|
StandardPaths.PicturesLocation)[0]
|
||||||
|
|
||||||
|
readonly property url data: `${StandardPaths.standardLocations(
|
||||||
|
StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
||||||
|
readonly property url state: `${StandardPaths.standardLocations(
|
||||||
|
StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
||||||
|
readonly property url cache: `${StandardPaths.standardLocations(
|
||||||
|
StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell`
|
||||||
|
readonly property url config: `${StandardPaths.standardLocations(
|
||||||
|
StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell`
|
||||||
|
|
||||||
|
readonly property url imagecache: `${cache}/imagecache`
|
||||||
|
|
||||||
|
function stringify(path: url): string {
|
||||||
|
return path.toString().replace(/%20/g, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandTilde(path: string): string {
|
||||||
|
return strip(path.replace("~", stringify(root.home)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortenHome(path: string): string {
|
||||||
|
return path.replace(strip(root.home), "~")
|
||||||
|
}
|
||||||
|
|
||||||
|
function strip(path: url): string {
|
||||||
|
return stringify(path).replace("file://", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkdir(path: url): void {
|
||||||
|
Quickshell.execDetached(["mkdir", "-p", strip(path)])
|
||||||
|
}
|
||||||
|
|
||||||
|
function copy(from: url, to: url): void {
|
||||||
|
Quickshell.execDetached(["cp", strip(from), strip(to)])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ! Spotify and maybe some other apps report the wrong app id in toplevels, hardcode special case
|
||||||
|
function moddedAppId(appId: string): string {
|
||||||
|
if (appId === "Spotify")
|
||||||
|
return "spotify-launcher"
|
||||||
|
if (appId === "beepertexts")
|
||||||
|
return "beeper"
|
||||||
|
if (appId === "home assistant desktop")
|
||||||
|
return "homeassistant-desktop"
|
||||||
|
if (appId.includes("com.transmissionbt.transmission"))
|
||||||
|
return "transmission-gtk"
|
||||||
|
return appId
|
||||||
|
}
|
||||||
|
}
|
||||||
9
quickshell/.config/quickshell/Common/Ref.qml
Normal file
9
quickshell/.config/quickshell/Common/Ref.qml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
required property Singleton service
|
||||||
|
|
||||||
|
Component.onCompleted: service.refCount++
|
||||||
|
Component.onDestruction: service.refCount--
|
||||||
|
}
|
||||||
597
quickshell/.config/quickshell/Common/SessionData.qml
Normal file
597
quickshell/.config/quickshell/Common/SessionData.qml
Normal file
|
|
@ -0,0 +1,597 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool isLightMode: false
|
||||||
|
property string wallpaperPath: ""
|
||||||
|
property string wallpaperLastPath: ""
|
||||||
|
property string profileLastPath: ""
|
||||||
|
property bool perMonitorWallpaper: false
|
||||||
|
property var monitorWallpapers: ({})
|
||||||
|
property bool doNotDisturb: false
|
||||||
|
property bool nightModeEnabled: false
|
||||||
|
property int nightModeTemperature: 4500
|
||||||
|
property bool nightModeAutoEnabled: false
|
||||||
|
property string nightModeAutoMode: "time"
|
||||||
|
|
||||||
|
property bool hasTriedDefaultSession: false
|
||||||
|
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||||
|
readonly property string _stateDir: _stateUrl.startsWith("file://") ? _stateUrl.substring(7) : _stateUrl
|
||||||
|
property int nightModeStartHour: 18
|
||||||
|
property int nightModeStartMinute: 0
|
||||||
|
property int nightModeEndHour: 6
|
||||||
|
property int nightModeEndMinute: 0
|
||||||
|
property real latitude: 0.0
|
||||||
|
property real longitude: 0.0
|
||||||
|
property string nightModeLocationProvider: ""
|
||||||
|
property var pinnedApps: []
|
||||||
|
property int selectedGpuIndex: 0
|
||||||
|
property bool nvidiaGpuTempEnabled: false
|
||||||
|
property bool nonNvidiaGpuTempEnabled: false
|
||||||
|
property var enabledGpuPciIds: []
|
||||||
|
property bool wallpaperCyclingEnabled: false
|
||||||
|
property string wallpaperCyclingMode: "interval" // "interval" or "time"
|
||||||
|
property int wallpaperCyclingInterval: 300 // seconds (5 minutes)
|
||||||
|
property string wallpaperCyclingTime: "06:00" // HH:mm format
|
||||||
|
property string lastBrightnessDevice: ""
|
||||||
|
property string notepadContent: ""
|
||||||
|
property string notepadCurrentFileName: ""
|
||||||
|
property string notepadCurrentFileUrl: ""
|
||||||
|
property string notepadLastSavedContent: ""
|
||||||
|
property var notepadTabs: []
|
||||||
|
property int notepadCurrentTabIndex: 0
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
loadSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
parseSettings(settingsFile.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSettings(content) {
|
||||||
|
try {
|
||||||
|
if (content && content.trim()) {
|
||||||
|
var settings = JSON.parse(content)
|
||||||
|
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
|
||||||
|
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
|
||||||
|
wallpaperLastPath = settings.wallpaperLastPath !== undefined ? settings.wallpaperLastPath : ""
|
||||||
|
profileLastPath = settings.profileLastPath !== undefined ? settings.profileLastPath : ""
|
||||||
|
perMonitorWallpaper = settings.perMonitorWallpaper !== undefined ? settings.perMonitorWallpaper : false
|
||||||
|
monitorWallpapers = settings.monitorWallpapers !== undefined ? settings.monitorWallpapers : {}
|
||||||
|
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false
|
||||||
|
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
|
||||||
|
nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500
|
||||||
|
nightModeAutoEnabled = settings.nightModeAutoEnabled !== undefined ? settings.nightModeAutoEnabled : false
|
||||||
|
nightModeAutoMode = settings.nightModeAutoMode !== undefined ? settings.nightModeAutoMode : "time"
|
||||||
|
// Handle legacy time format
|
||||||
|
if (settings.nightModeStartTime !== undefined) {
|
||||||
|
const parts = settings.nightModeStartTime.split(":")
|
||||||
|
nightModeStartHour = parseInt(parts[0]) || 18
|
||||||
|
nightModeStartMinute = parseInt(parts[1]) || 0
|
||||||
|
} else {
|
||||||
|
nightModeStartHour = settings.nightModeStartHour !== undefined ? settings.nightModeStartHour : 18
|
||||||
|
nightModeStartMinute = settings.nightModeStartMinute !== undefined ? settings.nightModeStartMinute : 0
|
||||||
|
}
|
||||||
|
if (settings.nightModeEndTime !== undefined) {
|
||||||
|
const parts = settings.nightModeEndTime.split(":")
|
||||||
|
nightModeEndHour = parseInt(parts[0]) || 6
|
||||||
|
nightModeEndMinute = parseInt(parts[1]) || 0
|
||||||
|
} else {
|
||||||
|
nightModeEndHour = settings.nightModeEndHour !== undefined ? settings.nightModeEndHour : 6
|
||||||
|
nightModeEndMinute = settings.nightModeEndMinute !== undefined ? settings.nightModeEndMinute : 0
|
||||||
|
}
|
||||||
|
latitude = settings.latitude !== undefined ? settings.latitude : 0.0
|
||||||
|
longitude = settings.longitude !== undefined ? settings.longitude : 0.0
|
||||||
|
nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : ""
|
||||||
|
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
|
||||||
|
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
|
||||||
|
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false
|
||||||
|
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false
|
||||||
|
enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []
|
||||||
|
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled !== undefined ? settings.wallpaperCyclingEnabled : false
|
||||||
|
wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval"
|
||||||
|
wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300
|
||||||
|
wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00"
|
||||||
|
lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : ""
|
||||||
|
notepadContent = settings.notepadContent !== undefined ? settings.notepadContent : ""
|
||||||
|
|
||||||
|
// Generate system themes but don't override user's theme choice
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
notepadCurrentFileName = settings.notepadCurrentFileName !== undefined ? settings.notepadCurrentFileName : ""
|
||||||
|
notepadCurrentFileUrl = settings.notepadCurrentFileUrl !== undefined ? settings.notepadCurrentFileUrl : ""
|
||||||
|
notepadLastSavedContent = settings.notepadLastSavedContent !== undefined ? settings.notepadLastSavedContent : ""
|
||||||
|
notepadTabs = settings.notepadTabs !== undefined ? settings.notepadTabs : []
|
||||||
|
notepadCurrentTabIndex = settings.notepadCurrentTabIndex !== undefined ? settings.notepadCurrentTabIndex : 0
|
||||||
|
|
||||||
|
// Migrate legacy single notepad to tabs if needed
|
||||||
|
if (notepadTabs.length === 0 && (notepadContent || notepadCurrentFileName)) {
|
||||||
|
notepadTabs = [{
|
||||||
|
id: Date.now(),
|
||||||
|
title: notepadCurrentFileName || "Untitled",
|
||||||
|
content: notepadContent,
|
||||||
|
fileName: notepadCurrentFileName,
|
||||||
|
fileUrl: notepadCurrentFileUrl,
|
||||||
|
lastSavedContent: notepadLastSavedContent,
|
||||||
|
hasUnsavedChanges: false
|
||||||
|
}]
|
||||||
|
notepadCurrentTabIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure at least one tab exists
|
||||||
|
if (notepadTabs.length === 0) {
|
||||||
|
notepadTabs = [{
|
||||||
|
id: Date.now(),
|
||||||
|
title: "Untitled",
|
||||||
|
content: "",
|
||||||
|
fileName: "",
|
||||||
|
fileUrl: "",
|
||||||
|
lastSavedContent: "",
|
||||||
|
hasUnsavedChanges: false
|
||||||
|
}]
|
||||||
|
notepadCurrentTabIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
settingsFile.setText(JSON.stringify({
|
||||||
|
"isLightMode": isLightMode,
|
||||||
|
"wallpaperPath": wallpaperPath,
|
||||||
|
"wallpaperLastPath": wallpaperLastPath,
|
||||||
|
"profileLastPath": profileLastPath,
|
||||||
|
"perMonitorWallpaper": perMonitorWallpaper,
|
||||||
|
"monitorWallpapers": monitorWallpapers,
|
||||||
|
"doNotDisturb": doNotDisturb,
|
||||||
|
"nightModeEnabled": nightModeEnabled,
|
||||||
|
"nightModeTemperature": nightModeTemperature,
|
||||||
|
"nightModeAutoEnabled": nightModeAutoEnabled,
|
||||||
|
"nightModeAutoMode": nightModeAutoMode,
|
||||||
|
"nightModeStartHour": nightModeStartHour,
|
||||||
|
"nightModeStartMinute": nightModeStartMinute,
|
||||||
|
"nightModeEndHour": nightModeEndHour,
|
||||||
|
"nightModeEndMinute": nightModeEndMinute,
|
||||||
|
"latitude": latitude,
|
||||||
|
"longitude": longitude,
|
||||||
|
"nightModeLocationProvider": nightModeLocationProvider,
|
||||||
|
"pinnedApps": pinnedApps,
|
||||||
|
"selectedGpuIndex": selectedGpuIndex,
|
||||||
|
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
|
||||||
|
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
|
||||||
|
"enabledGpuPciIds": enabledGpuPciIds,
|
||||||
|
"wallpaperCyclingEnabled": wallpaperCyclingEnabled,
|
||||||
|
"wallpaperCyclingMode": wallpaperCyclingMode,
|
||||||
|
"wallpaperCyclingInterval": wallpaperCyclingInterval,
|
||||||
|
"wallpaperCyclingTime": wallpaperCyclingTime,
|
||||||
|
"lastBrightnessDevice": lastBrightnessDevice,
|
||||||
|
"notepadContent": notepadContent,
|
||||||
|
"notepadCurrentFileName": notepadCurrentFileName,
|
||||||
|
"notepadCurrentFileUrl": notepadCurrentFileUrl,
|
||||||
|
"notepadLastSavedContent": notepadLastSavedContent,
|
||||||
|
"notepadTabs": notepadTabs,
|
||||||
|
"notepadCurrentTabIndex": notepadCurrentTabIndex
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLightMode(lightMode) {
|
||||||
|
isLightMode = lightMode
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDoNotDisturb(enabled) {
|
||||||
|
doNotDisturb = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeEnabled(enabled) {
|
||||||
|
nightModeEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeTemperature(temperature) {
|
||||||
|
nightModeTemperature = temperature
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeAutoEnabled(enabled) {
|
||||||
|
console.log("SessionData: Setting nightModeAutoEnabled to", enabled)
|
||||||
|
nightModeAutoEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeAutoMode(mode) {
|
||||||
|
nightModeAutoMode = mode
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeStartHour(hour) {
|
||||||
|
nightModeStartHour = hour
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeStartMinute(minute) {
|
||||||
|
nightModeStartMinute = minute
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeEndHour(hour) {
|
||||||
|
nightModeEndHour = hour
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeEndMinute(minute) {
|
||||||
|
nightModeEndMinute = minute
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLatitude(lat) {
|
||||||
|
console.log("SessionData: Setting latitude to", lat)
|
||||||
|
latitude = lat
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLongitude(lng) {
|
||||||
|
console.log("SessionData: Setting longitude to", lng)
|
||||||
|
longitude = lng
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNightModeLocationProvider(provider) {
|
||||||
|
nightModeLocationProvider = provider
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperPath(path) {
|
||||||
|
wallpaperPath = path
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaper(imagePath) {
|
||||||
|
wallpaperPath = imagePath
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
if (Theme.currentTheme === Theme.dynamic) {
|
||||||
|
Theme.extractColors()
|
||||||
|
}
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperColor(color) {
|
||||||
|
wallpaperPath = color
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
if (Theme.currentTheme === Theme.dynamic) {
|
||||||
|
Theme.extractColors()
|
||||||
|
}
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearWallpaper() {
|
||||||
|
wallpaperPath = ""
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.theme) {
|
||||||
|
Theme.switchTheme(SettingsData.theme)
|
||||||
|
} else {
|
||||||
|
Theme.switchTheme("blue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperLastPath(path) {
|
||||||
|
wallpaperLastPath = path
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProfileLastPath(path) {
|
||||||
|
profileLastPath = path
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPinnedApps(apps) {
|
||||||
|
pinnedApps = apps
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPinnedApp(appId) {
|
||||||
|
if (!appId)
|
||||||
|
return
|
||||||
|
var currentPinned = [...pinnedApps]
|
||||||
|
if (currentPinned.indexOf(appId) === -1) {
|
||||||
|
currentPinned.push(appId)
|
||||||
|
setPinnedApps(currentPinned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePinnedApp(appId) {
|
||||||
|
if (!appId)
|
||||||
|
return
|
||||||
|
var currentPinned = pinnedApps.filter(id => id !== appId)
|
||||||
|
setPinnedApps(currentPinned)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPinnedApp(appId) {
|
||||||
|
return appId && pinnedApps.indexOf(appId) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedGpuIndex(index) {
|
||||||
|
selectedGpuIndex = index
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNvidiaGpuTempEnabled(enabled) {
|
||||||
|
nvidiaGpuTempEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNonNvidiaGpuTempEnabled(enabled) {
|
||||||
|
nonNvidiaGpuTempEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEnabledGpuPciIds(pciIds) {
|
||||||
|
enabledGpuPciIds = pciIds
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperCyclingEnabled(enabled) {
|
||||||
|
wallpaperCyclingEnabled = enabled
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperCyclingMode(mode) {
|
||||||
|
wallpaperCyclingMode = mode
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperCyclingInterval(interval) {
|
||||||
|
wallpaperCyclingInterval = interval
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWallpaperCyclingTime(time) {
|
||||||
|
wallpaperCyclingTime = time
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPerMonitorWallpaper(enabled) {
|
||||||
|
perMonitorWallpaper = enabled
|
||||||
|
|
||||||
|
// Disable automatic cycling when per-monitor mode is enabled
|
||||||
|
if (enabled && wallpaperCyclingEnabled) {
|
||||||
|
wallpaperCyclingEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
// Refresh dynamic theming when per-monitor mode changes
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMonitorWallpaper(screenName, path) {
|
||||||
|
var newMonitorWallpapers = Object.assign({}, monitorWallpapers)
|
||||||
|
if (path && path !== "") {
|
||||||
|
newMonitorWallpapers[screenName] = path
|
||||||
|
} else {
|
||||||
|
delete newMonitorWallpapers[screenName]
|
||||||
|
}
|
||||||
|
monitorWallpapers = newMonitorWallpapers
|
||||||
|
saveSettings()
|
||||||
|
|
||||||
|
// Trigger dynamic theming if this is the first monitor and dynamic theming is enabled
|
||||||
|
if (typeof Theme !== "undefined" && typeof Quickshell !== "undefined") {
|
||||||
|
var screens = Quickshell.screens
|
||||||
|
if (screens.length > 0 && screenName === screens[0].name) {
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.wallpaperDynamicTheming) {
|
||||||
|
Theme.switchTheme("dynamic")
|
||||||
|
Theme.extractColors()
|
||||||
|
}
|
||||||
|
Theme.generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonitorWallpaper(screenName) {
|
||||||
|
if (!perMonitorWallpaper) {
|
||||||
|
return wallpaperPath
|
||||||
|
}
|
||||||
|
return monitorWallpapers[screenName] || wallpaperPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLastBrightnessDevice(device) {
|
||||||
|
lastBrightnessDevice = device
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: settingsFile
|
||||||
|
|
||||||
|
path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
||||||
|
blockLoading: true
|
||||||
|
blockWrites: true
|
||||||
|
watchChanges: true
|
||||||
|
onLoaded: {
|
||||||
|
parseSettings(settingsFile.text())
|
||||||
|
hasTriedDefaultSession = false
|
||||||
|
}
|
||||||
|
onLoadFailed: error => {
|
||||||
|
if (!hasTriedDefaultSession) {
|
||||||
|
hasTriedDefaultSession = true
|
||||||
|
defaultSessionCheckProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: defaultSessionCheckProcess
|
||||||
|
|
||||||
|
command: ["sh", "-c", "CONFIG_DIR=\"" + _stateDir
|
||||||
|
+ "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-session.json\" ] && [ ! -f \"$CONFIG_DIR/session.json\" ]; then cp \"$CONFIG_DIR/default-session.json\" \"$CONFIG_DIR/session.json\" && echo 'copied'; else echo 'not_found'; fi"]
|
||||||
|
running: false
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
console.log("Copied default-session.json to session.json")
|
||||||
|
settingsFile.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "wallpaper"
|
||||||
|
|
||||||
|
function get(): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead."
|
||||||
|
}
|
||||||
|
return root.wallpaperPath || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(path: string): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return "ERROR: No path provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
||||||
|
|
||||||
|
try {
|
||||||
|
root.setWallpaper(absolutePath)
|
||||||
|
return "SUCCESS: Wallpaper set to " + absolutePath
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to set wallpaper: " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear(): string {
|
||||||
|
root.setWallpaper("")
|
||||||
|
root.setPerMonitorWallpaper(false)
|
||||||
|
root.monitorWallpapers = {}
|
||||||
|
root.saveSettings()
|
||||||
|
return "SUCCESS: All wallpapers cleared"
|
||||||
|
}
|
||||||
|
|
||||||
|
function next(): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use nextFor(screenName) instead."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root.wallpaperPath) {
|
||||||
|
return "ERROR: No wallpaper set"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WallpaperCyclingService.cycleNextManually()
|
||||||
|
return "SUCCESS: Cycling to next wallpaper"
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to cycle wallpaper: " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prev(): string {
|
||||||
|
if (root.perMonitorWallpaper) {
|
||||||
|
return "ERROR: Per-monitor mode enabled. Use prevFor(screenName) instead."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root.wallpaperPath) {
|
||||||
|
return "ERROR: No wallpaper set"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WallpaperCyclingService.cyclePrevManually()
|
||||||
|
return "SUCCESS: Cycling to previous wallpaper"
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to cycle wallpaper: " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFor(screenName: string): string {
|
||||||
|
if (!screenName) {
|
||||||
|
return "ERROR: No screen name provided"
|
||||||
|
}
|
||||||
|
return root.getMonitorWallpaper(screenName) || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFor(screenName: string, path: string): string {
|
||||||
|
if (!screenName) {
|
||||||
|
return "ERROR: No screen name provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return "ERROR: No path provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!root.perMonitorWallpaper) {
|
||||||
|
root.setPerMonitorWallpaper(true)
|
||||||
|
}
|
||||||
|
root.setMonitorWallpaper(screenName, absolutePath)
|
||||||
|
return "SUCCESS: Wallpaper set for " + screenName + " to " + absolutePath
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to set wallpaper for " + screenName + ": " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextFor(screenName: string): string {
|
||||||
|
if (!screenName) {
|
||||||
|
return "ERROR: No screen name provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
||||||
|
if (!currentWallpaper) {
|
||||||
|
return "ERROR: No wallpaper set for " + screenName
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WallpaperCyclingService.cycleNextForMonitor(screenName)
|
||||||
|
return "SUCCESS: Cycling to next wallpaper for " + screenName
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevFor(screenName: string): string {
|
||||||
|
if (!screenName) {
|
||||||
|
return "ERROR: No screen name provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentWallpaper = root.getMonitorWallpaper(screenName)
|
||||||
|
if (!currentWallpaper) {
|
||||||
|
return "ERROR: No wallpaper set for " + screenName
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WallpaperCyclingService.cyclePrevForMonitor(screenName)
|
||||||
|
return "SUCCESS: Cycling to previous wallpaper for " + screenName
|
||||||
|
} catch (e) {
|
||||||
|
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1123
quickshell/.config/quickshell/Common/SettingsData.qml
Normal file
1123
quickshell/.config/quickshell/Common/SettingsData.qml
Normal file
File diff suppressed because it is too large
Load diff
380
quickshell/.config/quickshell/Common/StockThemes.js
Normal file
380
quickshell/.config/quickshell/Common/StockThemes.js
Normal file
|
|
@ -0,0 +1,380 @@
|
||||||
|
// Stock theme definitions for DankMaterialShell
|
||||||
|
// Separated from Theme.qml to keep that file clean
|
||||||
|
|
||||||
|
const StockThemes = {
|
||||||
|
DARK: {
|
||||||
|
blue: {
|
||||||
|
name: "Blue",
|
||||||
|
primary: "#42a5f5",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#1976d2",
|
||||||
|
secondary: "#8ab4f8",
|
||||||
|
surface: "#1a1c1e",
|
||||||
|
surfaceText: "#e3e8ef",
|
||||||
|
surfaceVariant: "#44464f",
|
||||||
|
surfaceVariantText: "#c4c7c5",
|
||||||
|
surfaceTint: "#8ab4f8",
|
||||||
|
background: "#1a1c1e",
|
||||||
|
backgroundText: "#e3e8ef",
|
||||||
|
outline: "#8e918f",
|
||||||
|
surfaceContainer: "#1e2023",
|
||||||
|
surfaceContainerHigh: "#292b2f"
|
||||||
|
},
|
||||||
|
deepBlue: {
|
||||||
|
name: "Deep Blue",
|
||||||
|
primary: "#0061a4",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#004881",
|
||||||
|
secondary: "#42a5f5",
|
||||||
|
surface: "#1a1c1e",
|
||||||
|
surfaceText: "#e3e8ef",
|
||||||
|
surfaceVariant: "#44464f",
|
||||||
|
surfaceVariantText: "#c4c7c5",
|
||||||
|
surfaceTint: "#8ab4f8",
|
||||||
|
background: "#1a1c1e",
|
||||||
|
backgroundText: "#e3e8ef",
|
||||||
|
outline: "#8e918f",
|
||||||
|
surfaceContainer: "#1e2023",
|
||||||
|
surfaceContainerHigh: "#292b2f"
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
name: "Purple",
|
||||||
|
primary: "#D0BCFF",
|
||||||
|
primaryText: "#381E72",
|
||||||
|
primaryContainer: "#4F378B",
|
||||||
|
secondary: "#CCC2DC",
|
||||||
|
surface: "#10121E",
|
||||||
|
surfaceText: "#E6E0E9",
|
||||||
|
surfaceVariant: "#49454F",
|
||||||
|
surfaceVariantText: "#CAC4D0",
|
||||||
|
surfaceTint: "#D0BCFF",
|
||||||
|
background: "#10121E",
|
||||||
|
backgroundText: "#E6E0E9",
|
||||||
|
outline: "#938F99",
|
||||||
|
surfaceContainer: "#1D1B20",
|
||||||
|
surfaceContainerHigh: "#2B2930"
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
name: "Green",
|
||||||
|
primary: "#4caf50",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#388e3c",
|
||||||
|
secondary: "#81c995",
|
||||||
|
surface: "#0f1411",
|
||||||
|
surfaceText: "#e1f5e3",
|
||||||
|
surfaceVariant: "#404943",
|
||||||
|
surfaceVariantText: "#c1cbc4",
|
||||||
|
surfaceTint: "#81c995",
|
||||||
|
background: "#0f1411",
|
||||||
|
backgroundText: "#e1f5e3",
|
||||||
|
outline: "#8b938c",
|
||||||
|
surfaceContainer: "#1a1f1b",
|
||||||
|
surfaceContainerHigh: "#252a26"
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
name: "Orange",
|
||||||
|
primary: "#ff6d00",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#e65100",
|
||||||
|
secondary: "#ffb74d",
|
||||||
|
surface: "#1c1410",
|
||||||
|
surfaceText: "#f5f1ea",
|
||||||
|
surfaceVariant: "#4a453a",
|
||||||
|
surfaceVariantText: "#cbc5b8",
|
||||||
|
surfaceTint: "#ffb74d",
|
||||||
|
background: "#1c1410",
|
||||||
|
backgroundText: "#f5f1ea",
|
||||||
|
outline: "#958f84",
|
||||||
|
surfaceContainer: "#211e17",
|
||||||
|
surfaceContainerHigh: "#2c291f"
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
name: "Red",
|
||||||
|
primary: "#f44336",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#d32f2f",
|
||||||
|
secondary: "#f28b82",
|
||||||
|
surface: "#1c1011",
|
||||||
|
surfaceText: "#f5e8ea",
|
||||||
|
surfaceVariant: "#4a3f41",
|
||||||
|
surfaceVariantText: "#cbc2c4",
|
||||||
|
surfaceTint: "#f28b82",
|
||||||
|
background: "#1c1011",
|
||||||
|
backgroundText: "#f5e8ea",
|
||||||
|
outline: "#958b8d",
|
||||||
|
surfaceContainer: "#211b1c",
|
||||||
|
surfaceContainerHigh: "#2c2426"
|
||||||
|
},
|
||||||
|
cyan: {
|
||||||
|
name: "Cyan",
|
||||||
|
primary: "#00bcd4",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#0097a7",
|
||||||
|
secondary: "#4dd0e1",
|
||||||
|
surface: "#0f1617",
|
||||||
|
surfaceText: "#e8f4f5",
|
||||||
|
surfaceVariant: "#3f474a",
|
||||||
|
surfaceVariantText: "#c2c9cb",
|
||||||
|
surfaceTint: "#4dd0e1",
|
||||||
|
background: "#0f1617",
|
||||||
|
backgroundText: "#e8f4f5",
|
||||||
|
outline: "#8c9194",
|
||||||
|
surfaceContainer: "#1a1f20",
|
||||||
|
surfaceContainerHigh: "#252b2c"
|
||||||
|
},
|
||||||
|
pink: {
|
||||||
|
name: "Pink",
|
||||||
|
primary: "#e91e63",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#c2185b",
|
||||||
|
secondary: "#f8bbd9",
|
||||||
|
surface: "#1a1014",
|
||||||
|
surfaceText: "#f3e8ee",
|
||||||
|
surfaceVariant: "#483f45",
|
||||||
|
surfaceVariantText: "#c9c2c7",
|
||||||
|
surfaceTint: "#f8bbd9",
|
||||||
|
background: "#1a1014",
|
||||||
|
backgroundText: "#f3e8ee",
|
||||||
|
outline: "#938a90",
|
||||||
|
surfaceContainer: "#1f1b1e",
|
||||||
|
surfaceContainerHigh: "#2a2428"
|
||||||
|
},
|
||||||
|
amber: {
|
||||||
|
name: "Amber",
|
||||||
|
primary: "#ffc107",
|
||||||
|
primaryText: "#000000",
|
||||||
|
primaryContainer: "#ff8f00",
|
||||||
|
secondary: "#ffd54f",
|
||||||
|
surface: "#1a1710",
|
||||||
|
surfaceText: "#f3f0e8",
|
||||||
|
surfaceVariant: "#49453a",
|
||||||
|
surfaceVariantText: "#cac5b8",
|
||||||
|
surfaceTint: "#ffd54f",
|
||||||
|
background: "#1a1710",
|
||||||
|
backgroundText: "#f3f0e8",
|
||||||
|
outline: "#949084",
|
||||||
|
surfaceContainer: "#1f1e17",
|
||||||
|
surfaceContainerHigh: "#2a281f"
|
||||||
|
},
|
||||||
|
coral: {
|
||||||
|
name: "Coral",
|
||||||
|
primary: "#ffb4ab",
|
||||||
|
primaryText: "#5f1412",
|
||||||
|
primaryContainer: "#8c1d18",
|
||||||
|
secondary: "#f9dedc",
|
||||||
|
surface: "#1a1110",
|
||||||
|
surfaceText: "#f1e8e7",
|
||||||
|
surfaceVariant: "#4a4142",
|
||||||
|
surfaceVariantText: "#cdc2c1",
|
||||||
|
surfaceTint: "#ffb4ab",
|
||||||
|
background: "#1a1110",
|
||||||
|
backgroundText: "#f1e8e7",
|
||||||
|
outline: "#968b8a",
|
||||||
|
surfaceContainer: "#201a19",
|
||||||
|
surfaceContainerHigh: "#2b2221"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LIGHT: {
|
||||||
|
blue: {
|
||||||
|
name: "Blue Light",
|
||||||
|
primary: "#1976d2",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#e3f2fd",
|
||||||
|
secondary: "#42a5f5",
|
||||||
|
surface: "#fefefe",
|
||||||
|
surfaceText: "#1a1c1e",
|
||||||
|
surfaceVariant: "#e7e0ec",
|
||||||
|
surfaceVariantText: "#49454f",
|
||||||
|
surfaceTint: "#1976d2",
|
||||||
|
background: "#fefefe",
|
||||||
|
backgroundText: "#1a1c1e",
|
||||||
|
outline: "#79747e",
|
||||||
|
surfaceContainer: "#f3f3f3",
|
||||||
|
surfaceContainerHigh: "#ececec"
|
||||||
|
},
|
||||||
|
deepBlue: {
|
||||||
|
name: "Deep Blue Light",
|
||||||
|
primary: "#0061a4",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#cfe5ff",
|
||||||
|
secondary: "#1976d2",
|
||||||
|
surface: "#fefefe",
|
||||||
|
surfaceText: "#1a1c1e",
|
||||||
|
surfaceVariant: "#e7e0ec",
|
||||||
|
surfaceVariantText: "#49454f",
|
||||||
|
surfaceTint: "#0061a4",
|
||||||
|
background: "#fefefe",
|
||||||
|
backgroundText: "#1a1c1e",
|
||||||
|
outline: "#79747e",
|
||||||
|
surfaceContainer: "#f3f3f3",
|
||||||
|
surfaceContainerHigh: "#ececec"
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
name: "Purple Light",
|
||||||
|
primary: "#6750A4",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#EADDFF",
|
||||||
|
secondary: "#625B71",
|
||||||
|
surface: "#FFFBFE",
|
||||||
|
surfaceText: "#1C1B1F",
|
||||||
|
surfaceVariant: "#E7E0EC",
|
||||||
|
surfaceVariantText: "#49454F",
|
||||||
|
surfaceTint: "#6750A4",
|
||||||
|
background: "#FFFBFE",
|
||||||
|
backgroundText: "#1C1B1F",
|
||||||
|
outline: "#79747E",
|
||||||
|
surfaceContainer: "#F3EDF7",
|
||||||
|
surfaceContainerHigh: "#ECE6F0"
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
name: "Green Light",
|
||||||
|
primary: "#2e7d32",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#e8f5e8",
|
||||||
|
secondary: "#4caf50",
|
||||||
|
surface: "#fefefe",
|
||||||
|
surfaceText: "#1a1c1e",
|
||||||
|
surfaceVariant: "#e7e0ec",
|
||||||
|
surfaceVariantText: "#49454f",
|
||||||
|
surfaceTint: "#2e7d32",
|
||||||
|
background: "#fefefe",
|
||||||
|
backgroundText: "#1a1c1e",
|
||||||
|
outline: "#79747e",
|
||||||
|
surfaceContainer: "#f3f3f3",
|
||||||
|
surfaceContainerHigh: "#ececec"
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
name: "Orange Light",
|
||||||
|
primary: "#e65100",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#ffecb3",
|
||||||
|
secondary: "#ff9800",
|
||||||
|
surface: "#fefefe",
|
||||||
|
surfaceText: "#1a1c1e",
|
||||||
|
surfaceVariant: "#e7e0ec",
|
||||||
|
surfaceVariantText: "#49454f",
|
||||||
|
surfaceTint: "#e65100",
|
||||||
|
background: "#fefefe",
|
||||||
|
backgroundText: "#1a1c1e",
|
||||||
|
outline: "#79747e",
|
||||||
|
surfaceContainer: "#f3f3f3",
|
||||||
|
surfaceContainerHigh: "#ececec"
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
name: "Red Light",
|
||||||
|
primary: "#d32f2f",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#ffebee",
|
||||||
|
secondary: "#f44336",
|
||||||
|
surface: "#fefefe",
|
||||||
|
surfaceText: "#1a1c1e",
|
||||||
|
surfaceVariant: "#e7e0ec",
|
||||||
|
surfaceVariantText: "#49454f",
|
||||||
|
surfaceTint: "#d32f2f",
|
||||||
|
background: "#fefefe",
|
||||||
|
backgroundText: "#1a1c1e",
|
||||||
|
outline: "#79747e",
|
||||||
|
surfaceContainer: "#f3f3f3",
|
||||||
|
surfaceContainerHigh: "#ececec"
|
||||||
|
},
|
||||||
|
cyan: {
|
||||||
|
name: "Cyan Light",
|
||||||
|
primary: "#0097a7",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#e0f2f1",
|
||||||
|
secondary: "#00bcd4",
|
||||||
|
surface: "#fefefe",
|
||||||
|
surfaceText: "#1a1c1e",
|
||||||
|
surfaceVariant: "#e7e0ec",
|
||||||
|
surfaceVariantText: "#49454f",
|
||||||
|
surfaceTint: "#0097a7",
|
||||||
|
background: "#fefefe",
|
||||||
|
backgroundText: "#1a1c1e",
|
||||||
|
outline: "#79747e",
|
||||||
|
surfaceContainer: "#f3f3f3",
|
||||||
|
surfaceContainerHigh: "#ececec"
|
||||||
|
},
|
||||||
|
pink: {
|
||||||
|
name: "Pink Light",
|
||||||
|
primary: "#c2185b",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#fce4ec",
|
||||||
|
secondary: "#e91e63",
|
||||||
|
surface: "#fefefe",
|
||||||
|
surfaceText: "#1a1c1e",
|
||||||
|
surfaceVariant: "#e7e0ec",
|
||||||
|
surfaceVariantText: "#49454f",
|
||||||
|
surfaceTint: "#c2185b",
|
||||||
|
background: "#fefefe",
|
||||||
|
backgroundText: "#1a1c1e",
|
||||||
|
outline: "#79747e",
|
||||||
|
surfaceContainer: "#f3f3f3",
|
||||||
|
surfaceContainerHigh: "#ececec"
|
||||||
|
},
|
||||||
|
amber: {
|
||||||
|
name: "Amber Light",
|
||||||
|
primary: "#ff8f00",
|
||||||
|
primaryText: "#000000",
|
||||||
|
primaryContainer: "#fff8e1",
|
||||||
|
secondary: "#ffc107",
|
||||||
|
surface: "#fefefe",
|
||||||
|
surfaceText: "#1a1c1e",
|
||||||
|
surfaceVariant: "#e7e0ec",
|
||||||
|
surfaceVariantText: "#49454f",
|
||||||
|
surfaceTint: "#ff8f00",
|
||||||
|
background: "#fefefe",
|
||||||
|
backgroundText: "#1a1c1e",
|
||||||
|
outline: "#79747e",
|
||||||
|
surfaceContainer: "#f3f3f3",
|
||||||
|
surfaceContainerHigh: "#ececec"
|
||||||
|
},
|
||||||
|
coral: {
|
||||||
|
name: "Coral Light",
|
||||||
|
primary: "#8c1d18",
|
||||||
|
primaryText: "#ffffff",
|
||||||
|
primaryContainer: "#ffdad6",
|
||||||
|
secondary: "#ff5449",
|
||||||
|
surface: "#fefefe",
|
||||||
|
surfaceText: "#1a1c1e",
|
||||||
|
surfaceVariant: "#e7e0ec",
|
||||||
|
surfaceVariantText: "#49454f",
|
||||||
|
surfaceTint: "#8c1d18",
|
||||||
|
background: "#fefefe",
|
||||||
|
backgroundText: "#1a1c1e",
|
||||||
|
outline: "#79747e",
|
||||||
|
surfaceContainer: "#f3f3f3",
|
||||||
|
surfaceContainerHigh: "#ececec"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeNames = {
|
||||||
|
BLUE: "blue",
|
||||||
|
DEEP_BLUE: "deepBlue",
|
||||||
|
PURPLE: "purple",
|
||||||
|
GREEN: "green",
|
||||||
|
ORANGE: "orange",
|
||||||
|
RED: "red",
|
||||||
|
CYAN: "cyan",
|
||||||
|
PINK: "pink",
|
||||||
|
AMBER: "amber",
|
||||||
|
CORAL: "coral",
|
||||||
|
DYNAMIC: "dynamic"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStockTheme(themeName) {
|
||||||
|
return Object.keys(StockThemes.DARK).includes(themeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAvailableThemes(isLight = false) {
|
||||||
|
return isLight ? StockThemes.LIGHT : StockThemes.DARK
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThemeByName(themeName, isLight = false) {
|
||||||
|
const themes = getAvailableThemes(isLight)
|
||||||
|
return themes[themeName] || themes.blue
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllThemeNames() {
|
||||||
|
return Object.keys(StockThemes.DARK)
|
||||||
|
}
|
||||||
819
quickshell/.config/quickshell/Common/Theme.qml
Normal file
819
quickshell/.config/quickshell/Common/Theme.qml
Normal file
|
|
@ -0,0 +1,819 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
import qs.Services
|
||||||
|
import "StockThemes.js" as StockThemes
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string currentTheme: "blue"
|
||||||
|
property bool isLightMode: false
|
||||||
|
|
||||||
|
readonly property string dynamic: "dynamic"
|
||||||
|
|
||||||
|
readonly property string homeDir: {
|
||||||
|
const url = StandardPaths.writableLocation(StandardPaths.HomeLocation).toString()
|
||||||
|
return url.startsWith("file://") ? url.substring(7) : url
|
||||||
|
}
|
||||||
|
readonly property string configDir: {
|
||||||
|
const url = StandardPaths.writableLocation(StandardPaths.ConfigLocation).toString()
|
||||||
|
return url.startsWith("file://") ? url.substring(7) : url
|
||||||
|
}
|
||||||
|
readonly property string shellDir: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Common/", "")
|
||||||
|
readonly property string wallpaperPath: {
|
||||||
|
if (typeof SessionData === "undefined") return ""
|
||||||
|
|
||||||
|
if (SessionData.perMonitorWallpaper) {
|
||||||
|
// Use first monitor's wallpaper for dynamic theming
|
||||||
|
var screens = Quickshell.screens
|
||||||
|
if (screens.length > 0) {
|
||||||
|
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name)
|
||||||
|
return firstMonitorWallpaper || SessionData.wallpaperPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SessionData.wallpaperPath
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool matugenAvailable: false
|
||||||
|
property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false
|
||||||
|
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
|
||||||
|
property var workerRunning: false
|
||||||
|
property var matugenColors: ({})
|
||||||
|
property bool extractionRequested: false
|
||||||
|
property int colorUpdateTrigger: 0
|
||||||
|
property var customThemeData: null
|
||||||
|
|
||||||
|
readonly property string stateDir: {
|
||||||
|
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()
|
||||||
|
const path = cacheHome.startsWith("file://") ? cacheHome.substring(7) : cacheHome
|
||||||
|
return path + "/dankshell"
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Quickshell.execDetached(["mkdir", "-p", stateDir])
|
||||||
|
matugenCheck.running = true
|
||||||
|
if (typeof SessionData !== "undefined")
|
||||||
|
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
|
||||||
|
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
|
||||||
|
switchTheme(SettingsData.currentThemeName, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMatugenColor(path, fallback) {
|
||||||
|
colorUpdateTrigger
|
||||||
|
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
||||||
|
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode]
|
||||||
|
for (const part of path.split(".")) {
|
||||||
|
if (!cur || typeof cur !== "object" || !(part in cur))
|
||||||
|
return fallback
|
||||||
|
cur = cur[part]
|
||||||
|
}
|
||||||
|
return cur || fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var currentThemeData: {
|
||||||
|
if (currentTheme === "custom") {
|
||||||
|
return customThemeData || StockThemes.getThemeByName("blue", isLightMode)
|
||||||
|
} else if (currentTheme === dynamic) {
|
||||||
|
return {
|
||||||
|
"primary": getMatugenColor("primary", "#42a5f5"),
|
||||||
|
"primaryText": getMatugenColor("on_primary", "#ffffff"),
|
||||||
|
"primaryContainer": getMatugenColor("primary_container", "#1976d2"),
|
||||||
|
"secondary": getMatugenColor("secondary", "#8ab4f8"),
|
||||||
|
"surface": getMatugenColor("surface", "#1a1c1e"),
|
||||||
|
"surfaceText": getMatugenColor("on_background", "#e3e8ef"),
|
||||||
|
"surfaceVariant": getMatugenColor("surface_variant", "#44464f"),
|
||||||
|
"surfaceVariantText": getMatugenColor("on_surface_variant", "#c4c7c5"),
|
||||||
|
"surfaceTint": getMatugenColor("surface_tint", "#8ab4f8"),
|
||||||
|
"background": getMatugenColor("background", "#1a1c1e"),
|
||||||
|
"backgroundText": getMatugenColor("on_background", "#e3e8ef"),
|
||||||
|
"outline": getMatugenColor("outline", "#8e918f"),
|
||||||
|
"surfaceContainer": getMatugenColor("surface_container", "#1e2023"),
|
||||||
|
"surfaceContainerHigh": getMatugenColor("surface_container_high", "#292b2f"),
|
||||||
|
"error": "#F2B8B5",
|
||||||
|
"warning": "#FF9800",
|
||||||
|
"info": "#2196F3",
|
||||||
|
"success": "#4CAF50"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return StockThemes.getThemeByName(currentTheme, isLightMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property color primary: currentThemeData.primary
|
||||||
|
property color primaryText: currentThemeData.primaryText
|
||||||
|
property color primaryContainer: currentThemeData.primaryContainer
|
||||||
|
property color secondary: currentThemeData.secondary
|
||||||
|
property color surface: currentThemeData.surface
|
||||||
|
property color surfaceText: currentThemeData.surfaceText
|
||||||
|
property color surfaceVariant: currentThemeData.surfaceVariant
|
||||||
|
property color surfaceVariantText: currentThemeData.surfaceVariantText
|
||||||
|
property color surfaceTint: currentThemeData.surfaceTint
|
||||||
|
property color background: currentThemeData.background
|
||||||
|
property color backgroundText: currentThemeData.backgroundText
|
||||||
|
property color outline: currentThemeData.outline
|
||||||
|
property color surfaceContainer: currentThemeData.surfaceContainer
|
||||||
|
property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
|
||||||
|
|
||||||
|
property color error: currentThemeData.error || "#F2B8B5"
|
||||||
|
property color warning: currentThemeData.warning || "#FF9800"
|
||||||
|
property color info: currentThemeData.info || "#2196F3"
|
||||||
|
property color tempWarning: "#ff9933"
|
||||||
|
property color tempDanger: "#ff5555"
|
||||||
|
property color success: currentThemeData.success || "#4CAF50"
|
||||||
|
|
||||||
|
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
|
||||||
|
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08)
|
||||||
|
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16)
|
||||||
|
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
|
||||||
|
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
|
||||||
|
|
||||||
|
property color secondaryHover: Qt.rgba(secondary.r, secondary.g, secondary.b, 0.08)
|
||||||
|
|
||||||
|
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
|
||||||
|
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
|
||||||
|
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
|
||||||
|
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.1)
|
||||||
|
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
|
||||||
|
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
|
||||||
|
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
|
||||||
|
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
|
||||||
|
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
|
||||||
|
|
||||||
|
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
|
||||||
|
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, 0.05)
|
||||||
|
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, 0.08)
|
||||||
|
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12)
|
||||||
|
|
||||||
|
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
|
||||||
|
|
||||||
|
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
||||||
|
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
|
||||||
|
property int shorterDuration: 100
|
||||||
|
property int shortDuration: 150
|
||||||
|
property int mediumDuration: 300
|
||||||
|
property int longDuration: 500
|
||||||
|
property int extraLongDuration: 1000
|
||||||
|
property int standardEasing: Easing.OutCubic
|
||||||
|
property int emphasizedEasing: Easing.OutQuart
|
||||||
|
|
||||||
|
property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12
|
||||||
|
property real spacingXS: 4
|
||||||
|
property real spacingS: 8
|
||||||
|
property real spacingM: 12
|
||||||
|
property real spacingL: 16
|
||||||
|
property real spacingXL: 24
|
||||||
|
property real fontSizeSmall: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 12
|
||||||
|
property real fontSizeMedium: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 14
|
||||||
|
property real fontSizeLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 16
|
||||||
|
property real fontSizeXLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 20
|
||||||
|
property real barHeight: 48
|
||||||
|
property real iconSize: 24
|
||||||
|
property real iconSizeSmall: 16
|
||||||
|
property real iconSizeLarge: 32
|
||||||
|
|
||||||
|
property real panelTransparency: 0.85
|
||||||
|
property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.topBarWidgetTransparency !== undefined ? SettingsData.topBarWidgetTransparency : 0.85
|
||||||
|
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 0.92
|
||||||
|
|
||||||
|
function switchTheme(themeName, savePrefs = true) {
|
||||||
|
if (themeName === dynamic) {
|
||||||
|
currentTheme = dynamic
|
||||||
|
extractColors()
|
||||||
|
} else if (themeName === "custom") {
|
||||||
|
currentTheme = "custom"
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) {
|
||||||
|
loadCustomThemeFromFile(SettingsData.customThemeFile)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentTheme = themeName
|
||||||
|
}
|
||||||
|
if (savePrefs && typeof SettingsData !== "undefined")
|
||||||
|
SettingsData.setTheme(currentTheme)
|
||||||
|
|
||||||
|
generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLightMode(light, savePrefs = true) {
|
||||||
|
isLightMode = light
|
||||||
|
if (savePrefs && typeof SessionData !== "undefined")
|
||||||
|
SessionData.setLightMode(isLightMode)
|
||||||
|
PortalService.setLightMode(isLightMode)
|
||||||
|
generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLightMode(savePrefs = true) {
|
||||||
|
setLightMode(!isLightMode, savePrefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceGenerateSystemThemes() {
|
||||||
|
if (!matugenAvailable) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showWarning("matugen not available - cannot generate system themes")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAvailableThemes() {
|
||||||
|
return StockThemes.getAllThemeNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThemeDisplayName(themeName) {
|
||||||
|
const themeData = StockThemes.getThemeByName(themeName, isLightMode)
|
||||||
|
return themeData.name
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThemeColors(themeName) {
|
||||||
|
if (themeName === "custom" && customThemeData) {
|
||||||
|
return customThemeData
|
||||||
|
}
|
||||||
|
return StockThemes.getThemeByName(themeName, isLightMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCustomTheme(themeData) {
|
||||||
|
if (themeData.dark || themeData.light) {
|
||||||
|
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
||||||
|
const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light
|
||||||
|
customThemeData = selectedTheme
|
||||||
|
} else {
|
||||||
|
customThemeData = themeData
|
||||||
|
}
|
||||||
|
|
||||||
|
generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCustomThemeFromFile(filePath) {
|
||||||
|
customThemeFileView.path = filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
property alias availableThemeNames: root._availableThemeNames
|
||||||
|
readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
|
||||||
|
property string currentThemeName: currentTheme
|
||||||
|
|
||||||
|
function popupBackground() {
|
||||||
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentBackground() {
|
||||||
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
|
||||||
|
}
|
||||||
|
|
||||||
|
function panelBackground() {
|
||||||
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency)
|
||||||
|
}
|
||||||
|
|
||||||
|
function widgetBackground() {
|
||||||
|
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPopupBackgroundAlpha() {
|
||||||
|
return popupTransparency
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContentBackgroundAlpha() {
|
||||||
|
return popupTransparency
|
||||||
|
}
|
||||||
|
|
||||||
|
function isColorDark(c) {
|
||||||
|
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBatteryIcon(level, isCharging, batteryAvailable) {
|
||||||
|
if (!batteryAvailable)
|
||||||
|
return _getBatteryPowerProfileIcon()
|
||||||
|
|
||||||
|
if (isCharging) {
|
||||||
|
if (level >= 90)
|
||||||
|
return "battery_charging_full"
|
||||||
|
if (level >= 80)
|
||||||
|
return "battery_charging_90"
|
||||||
|
if (level >= 60)
|
||||||
|
return "battery_charging_80"
|
||||||
|
if (level >= 50)
|
||||||
|
return "battery_charging_60"
|
||||||
|
if (level >= 30)
|
||||||
|
return "battery_charging_50"
|
||||||
|
if (level >= 20)
|
||||||
|
return "battery_charging_30"
|
||||||
|
return "battery_charging_20"
|
||||||
|
} else {
|
||||||
|
if (level >= 95)
|
||||||
|
return "battery_full"
|
||||||
|
if (level >= 85)
|
||||||
|
return "battery_6_bar"
|
||||||
|
if (level >= 70)
|
||||||
|
return "battery_5_bar"
|
||||||
|
if (level >= 55)
|
||||||
|
return "battery_4_bar"
|
||||||
|
if (level >= 40)
|
||||||
|
return "battery_3_bar"
|
||||||
|
if (level >= 25)
|
||||||
|
return "battery_2_bar"
|
||||||
|
if (level >= 10)
|
||||||
|
return "battery_1_bar"
|
||||||
|
return "battery_alert"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getBatteryPowerProfileIcon() {
|
||||||
|
if (typeof PowerProfiles === "undefined")
|
||||||
|
return "balance"
|
||||||
|
|
||||||
|
switch (PowerProfiles.profile) {
|
||||||
|
case PowerProfile.PowerSaver:
|
||||||
|
return "energy_savings_leaf"
|
||||||
|
case PowerProfile.Performance:
|
||||||
|
return "rocket_launch"
|
||||||
|
default:
|
||||||
|
return "balance"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPowerProfileIcon(profile) {
|
||||||
|
switch (profile) {
|
||||||
|
case PowerProfile.PowerSaver:
|
||||||
|
return "battery_saver"
|
||||||
|
case PowerProfile.Balanced:
|
||||||
|
return "battery_std"
|
||||||
|
case PowerProfile.Performance:
|
||||||
|
return "flash_on"
|
||||||
|
default:
|
||||||
|
return "settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPowerProfileLabel(profile) {
|
||||||
|
switch (profile) {
|
||||||
|
case PowerProfile.PowerSaver:
|
||||||
|
return "Power Saver"
|
||||||
|
case PowerProfile.Balanced:
|
||||||
|
return "Balanced"
|
||||||
|
case PowerProfile.Performance:
|
||||||
|
return "Performance"
|
||||||
|
default:
|
||||||
|
return profile.charAt(0).toUpperCase() + profile.slice(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPowerProfileDescription(profile) {
|
||||||
|
switch (profile) {
|
||||||
|
case PowerProfile.PowerSaver:
|
||||||
|
return "Extend battery life"
|
||||||
|
case PowerProfile.Balanced:
|
||||||
|
return "Balance power and performance"
|
||||||
|
case PowerProfile.Performance:
|
||||||
|
return "Prioritize performance"
|
||||||
|
default:
|
||||||
|
return "Custom power profile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractColors() {
|
||||||
|
extractionRequested = true
|
||||||
|
if (matugenAvailable)
|
||||||
|
fileChecker.running = true
|
||||||
|
else
|
||||||
|
matugenCheck.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLightModeChanged() {
|
||||||
|
if (matugenColors && Object.keys(matugenColors).length > 0) {
|
||||||
|
colorUpdateTrigger++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTheme === "custom" && customThemeFileView.path) {
|
||||||
|
customThemeFileView.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
generateSystemThemesFromCurrentTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDesiredTheme(kind, value, isLight, iconTheme) {
|
||||||
|
if (!matugenAvailable) {
|
||||||
|
console.warn("matugen not available - cannot set system theme")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const desired = {
|
||||||
|
"kind": kind,
|
||||||
|
"value": value,
|
||||||
|
"mode": isLight ? "light" : "dark",
|
||||||
|
"iconTheme": iconTheme || "System Default"
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = JSON.stringify(desired)
|
||||||
|
const desiredPath = stateDir + "/matugen.desired.json"
|
||||||
|
|
||||||
|
Quickshell.execDetached(["sh", "-c", `mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF`])
|
||||||
|
workerRunning = true
|
||||||
|
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, "--run"]
|
||||||
|
systemThemeGenerator.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSystemThemesFromCurrentTheme() {
|
||||||
|
if (!matugenAvailable)
|
||||||
|
return
|
||||||
|
|
||||||
|
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||||
|
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
||||||
|
|
||||||
|
if (currentTheme === dynamic) {
|
||||||
|
if (!wallpaperPath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (wallpaperPath.startsWith("#")) {
|
||||||
|
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme)
|
||||||
|
} else {
|
||||||
|
setDesiredTheme("image", wallpaperPath, isLight, iconTheme)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let primaryColor
|
||||||
|
if (currentTheme === "custom") {
|
||||||
|
if (!customThemeData || !customThemeData.primary) {
|
||||||
|
console.warn("Custom theme data not available for system theme generation")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
primaryColor = customThemeData.primary
|
||||||
|
} else {
|
||||||
|
primaryColor = currentThemeData.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!primaryColor) {
|
||||||
|
console.warn("No primary color available for theme:", currentTheme)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDesiredTheme("hex", primaryColor, isLight, iconTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyGtkColors() {
|
||||||
|
if (!matugenAvailable) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("matugen not available - cannot apply GTK colors")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
|
||||||
|
gtkApplier.command = [shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir]
|
||||||
|
gtkApplier.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyQtColors() {
|
||||||
|
if (!matugenAvailable) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("matugen not available - cannot apply Qt colors")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
qtApplier.command = [shellDir + "/scripts/qt.sh", configDir]
|
||||||
|
qtApplier.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractJsonFromText(text) {
|
||||||
|
if (!text)
|
||||||
|
return null
|
||||||
|
|
||||||
|
const start = text.search(/[{\[]/)
|
||||||
|
if (start === -1)
|
||||||
|
return null
|
||||||
|
|
||||||
|
const open = text[start]
|
||||||
|
const pairs = {
|
||||||
|
"{": '}',
|
||||||
|
"[": ']'
|
||||||
|
}
|
||||||
|
const close = pairs[open]
|
||||||
|
if (!close)
|
||||||
|
return null
|
||||||
|
|
||||||
|
let inString = false
|
||||||
|
let escape = false
|
||||||
|
const stack = [open]
|
||||||
|
|
||||||
|
for (var i = start + 1; i < text.length; i++) {
|
||||||
|
const ch = text[i]
|
||||||
|
|
||||||
|
if (inString) {
|
||||||
|
if (escape) {
|
||||||
|
escape = false
|
||||||
|
} else if (ch === '\\') {
|
||||||
|
escape = true
|
||||||
|
} else if (ch === '"') {
|
||||||
|
inString = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === '"') {
|
||||||
|
inString = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ch === '{' || ch === '[') {
|
||||||
|
stack.push(ch)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ch === '}' || ch === ']') {
|
||||||
|
const last = stack.pop()
|
||||||
|
if (!last || pairs[last] !== ch) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (stack.length === 0) {
|
||||||
|
return text.slice(start, i + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: matugenCheck
|
||||||
|
command: ["which", "matugen"]
|
||||||
|
onExited: code => {
|
||||||
|
matugenAvailable = (code === 0)
|
||||||
|
if (!matugenAvailable) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "matugen_missing"
|
||||||
|
ToastService.showWarning("matugen not found - dynamic theming disabled")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (extractionRequested) {
|
||||||
|
fileChecker.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||||
|
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
|
||||||
|
|
||||||
|
if (currentTheme === dynamic) {
|
||||||
|
if (wallpaperPath) {
|
||||||
|
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||||
|
if (wallpaperPath.startsWith("#")) {
|
||||||
|
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme)
|
||||||
|
} else {
|
||||||
|
setDesiredTheme("image", wallpaperPath, isLight, iconTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let primaryColor
|
||||||
|
if (currentTheme === "custom") {
|
||||||
|
if (customThemeData && customThemeData.primary) {
|
||||||
|
primaryColor = customThemeData.primary
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
primaryColor = currentThemeData.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primaryColor) {
|
||||||
|
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
|
||||||
|
setDesiredTheme("hex", primaryColor, isLight, iconTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: fileChecker
|
||||||
|
command: ["test", "-r", wallpaperPath]
|
||||||
|
onExited: code => {
|
||||||
|
if (code === 0) {
|
||||||
|
matugenProcess.running = true
|
||||||
|
} else if (wallpaperPath.startsWith("#")) {
|
||||||
|
colorMatugenProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: matugenProcess
|
||||||
|
command: ["matugen", "image", wallpaperPath, "--json", "hex"]
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: matugenCollector
|
||||||
|
onStreamFinished: {
|
||||||
|
if (!matugenCollector.text) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Wallpaper Processing Failed: Empty JSON extracted from matugen output.")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const extractedJson = extractJsonFromText(matugenCollector.text)
|
||||||
|
if (!extractedJson) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Wallpaper Processing Failed: Invalid JSON extracted from matugen output.")
|
||||||
|
}
|
||||||
|
console.log("Raw matugen output:", matugenCollector.text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
root.matugenColors = JSON.parse(extractedJson)
|
||||||
|
root.colorUpdateTrigger++
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.clearWallpaperError()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Wallpaper processing failed (JSON parse error after extraction)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: code => {
|
||||||
|
if (code !== 0) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Matugen command failed with exit code " + code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: colorMatugenProcess
|
||||||
|
command: ["matugen", "color", "hex", wallpaperPath, "--json", "hex"]
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: colorMatugenCollector
|
||||||
|
onStreamFinished: {
|
||||||
|
if (!colorMatugenCollector.text) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Color Processing Failed: Empty JSON extracted from matugen output.")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const extractedJson = extractJsonFromText(colorMatugenCollector.text)
|
||||||
|
if (!extractedJson) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Color Processing Failed: Invalid JSON extracted from matugen output.")
|
||||||
|
}
|
||||||
|
console.log("Raw matugen output:", colorMatugenCollector.text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
root.matugenColors = JSON.parse(extractedJson)
|
||||||
|
root.colorUpdateTrigger++
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.clearWallpaperError()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Color processing failed (JSON parse error after extraction)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: code => {
|
||||||
|
if (code !== 0) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.wallpaperErrorStatus = "error"
|
||||||
|
ToastService.showError("Matugen color command failed with exit code " + code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: ensureStateDir
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: systemThemeGenerator
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
workerRunning = false
|
||||||
|
|
||||||
|
if (exitCode === 2) {
|
||||||
|
// Exit code 2 means wallpaper/color not found - this is expected on first run
|
||||||
|
console.log("Theme worker: wallpaper/color not found, skipping theme generation")
|
||||||
|
} else if (exitCode !== 0) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("Theme worker failed (" + exitCode + ")")
|
||||||
|
}
|
||||||
|
console.warn("Theme worker failed with exit code:", exitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: gtkApplier
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: gtkStdout
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
id: gtkStderr
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showInfo("GTK colors applied successfully")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("Failed to apply GTK colors: " + gtkStderr.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: qtApplier
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: qtStdout
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
id: qtStderr
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showInfo("Qt colors applied successfully")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("Failed to apply Qt colors: " + qtStderr.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: customThemeFileView
|
||||||
|
watchChanges: currentTheme === "custom"
|
||||||
|
|
||||||
|
function parseAndLoadTheme() {
|
||||||
|
try {
|
||||||
|
var themeData = JSON.parse(customThemeFileView.text())
|
||||||
|
loadCustomTheme(themeData)
|
||||||
|
} catch (e) {
|
||||||
|
ToastService.showError("Invalid JSON format: " + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
parseAndLoadTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileChanged: {
|
||||||
|
customThemeFileView.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoadFailed: function (error) {
|
||||||
|
if (typeof ToastService !== "undefined") {
|
||||||
|
ToastService.showError("Failed to read theme file: " + error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "theme"
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
root.toggleLightMode()
|
||||||
|
return root.isLightMode ? "light" : "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
function light(): string {
|
||||||
|
root.setLightMode(true)
|
||||||
|
return "light"
|
||||||
|
}
|
||||||
|
|
||||||
|
function dark(): string {
|
||||||
|
root.setLightMode(false)
|
||||||
|
return "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMode(): string {
|
||||||
|
return root.isLightMode ? "light" : "dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1307
quickshell/.config/quickshell/Common/fzf.js
Normal file
1307
quickshell/.config/quickshell/Common/fzf.js
Normal file
File diff suppressed because it is too large
Load diff
106
quickshell/.config/quickshell/Common/markdown2html.js
Normal file
106
quickshell/.config/quickshell/Common/markdown2html.js
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
.pragma library
|
||||||
|
// This exists only beacause I haven't been able to get linkColor to work with MarkdownText
|
||||||
|
// May not be necessary if that's possible tbh.
|
||||||
|
function markdownToHtml(text) {
|
||||||
|
if (!text) return "";
|
||||||
|
|
||||||
|
// Store code blocks and inline code to protect them from further processing
|
||||||
|
const codeBlocks = [];
|
||||||
|
const inlineCode = [];
|
||||||
|
let blockIndex = 0;
|
||||||
|
let inlineIndex = 0;
|
||||||
|
|
||||||
|
// First, extract and replace code blocks with placeholders
|
||||||
|
let html = text.replace(/```([\s\S]*?)```/g, (match, code) => {
|
||||||
|
// Trim leading and trailing blank lines only
|
||||||
|
const trimmedCode = code.replace(/^\n+|\n+$/g, '');
|
||||||
|
// Escape HTML entities in code
|
||||||
|
const escapedCode = trimmedCode.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
codeBlocks.push(`<pre><code>${escapedCode}</code></pre>`);
|
||||||
|
return `\x00CODEBLOCK${blockIndex++}\x00`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract and replace inline code
|
||||||
|
html = html.replace(/`([^`]+)`/g, (match, code) => {
|
||||||
|
// Escape HTML entities in code
|
||||||
|
const escapedCode = code.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
inlineCode.push(`<code>${escapedCode}</code>`);
|
||||||
|
return `\x00INLINECODE${inlineIndex++}\x00`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now process everything else
|
||||||
|
// Escape HTML entities (but not in code blocks)
|
||||||
|
html = html.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
html = html.replace(/^### (.*?)$/gm, '<h3>$1</h3>');
|
||||||
|
html = html.replace(/^## (.*?)$/gm, '<h2>$1</h2>');
|
||||||
|
html = html.replace(/^# (.*?)$/gm, '<h1>$1</h1>');
|
||||||
|
|
||||||
|
// Bold and italic (order matters!)
|
||||||
|
html = html.replace(/\*\*\*(.*?)\*\*\*/g, '<b><i>$1</i></b>');
|
||||||
|
html = html.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||||
|
html = html.replace(/\*(.*?)\*/g, '<i>$1</i>');
|
||||||
|
html = html.replace(/___(.*?)___/g, '<b><i>$1</i></b>');
|
||||||
|
html = html.replace(/__(.*?)__/g, '<b>$1</b>');
|
||||||
|
html = html.replace(/_(.*?)_/g, '<i>$1</i>');
|
||||||
|
|
||||||
|
// Links
|
||||||
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
||||||
|
|
||||||
|
// Lists
|
||||||
|
html = html.replace(/^\* (.*?)$/gm, '<li>$1</li>');
|
||||||
|
html = html.replace(/^- (.*?)$/gm, '<li>$1</li>');
|
||||||
|
html = html.replace(/^\d+\. (.*?)$/gm, '<li>$1</li>');
|
||||||
|
|
||||||
|
// Wrap consecutive list items in ul/ol tags
|
||||||
|
html = html.replace(/(<li>[\s\S]*?<\/li>\s*)+/g, function(match) {
|
||||||
|
return '<ul>' + match + '</ul>';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detect plain URLs and wrap them in anchor tags (but not inside existing <a> or markdown links)
|
||||||
|
html = html.replace(/(^|[^"'>])((https?|file):\/\/[^\s<]+)/g, '$1<a href="$2">$2</a>');
|
||||||
|
|
||||||
|
// Restore code blocks and inline code BEFORE line break processing
|
||||||
|
html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => {
|
||||||
|
return codeBlocks[parseInt(index)];
|
||||||
|
});
|
||||||
|
|
||||||
|
html = html.replace(/\x00INLINECODE(\d+)\x00/g, (match, index) => {
|
||||||
|
return inlineCode[parseInt(index)];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Line breaks (after code blocks are restored)
|
||||||
|
html = html.replace(/\n\n/g, '</p><p>');
|
||||||
|
html = html.replace(/\n/g, '<br/>');
|
||||||
|
|
||||||
|
// Wrap in paragraph tags if not already wrapped
|
||||||
|
if (!html.startsWith('<')) {
|
||||||
|
html = '<p>' + html + '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the final HTML
|
||||||
|
// Remove <br/> tags immediately before block elements
|
||||||
|
html = html.replace(/<br\/>\s*<pre>/g, '<pre>');
|
||||||
|
html = html.replace(/<br\/>\s*<ul>/g, '<ul>');
|
||||||
|
html = html.replace(/<br\/>\s*<h[1-6]>/g, '<h$1>');
|
||||||
|
|
||||||
|
// Remove empty paragraphs
|
||||||
|
html = html.replace(/<p>\s*<\/p>/g, '');
|
||||||
|
html = html.replace(/<p>\s*<br\/>\s*<\/p>/g, '');
|
||||||
|
|
||||||
|
// Remove excessive line breaks
|
||||||
|
html = html.replace(/(<br\/>){3,}/g, '<br/><br/>'); // Max 2 consecutive line breaks
|
||||||
|
html = html.replace(/(<\/p>)\s*(<p>)/g, '$1$2'); // Remove whitespace between paragraphs
|
||||||
|
|
||||||
|
// Remove leading/trailing whitespace
|
||||||
|
html = html.trim();
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
readonly property int previewLength: 100
|
||||||
|
readonly property int longTextThreshold: 200
|
||||||
|
readonly property int modalWidth: 650
|
||||||
|
readonly property int modalHeight: 550
|
||||||
|
readonly property int itemHeight: 72
|
||||||
|
readonly property int thumbnailSize: 48
|
||||||
|
readonly property int retryInterval: 50
|
||||||
|
readonly property int viewportBuffer: 100
|
||||||
|
readonly property int extendedBuffer: 200
|
||||||
|
readonly property int keyboardHintsHeight: 80
|
||||||
|
readonly property int headerHeight: 40
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals.Clipboard
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: clipboardContent
|
||||||
|
|
||||||
|
required property var modal
|
||||||
|
required property var filteredModel
|
||||||
|
required property var clearConfirmDialog
|
||||||
|
|
||||||
|
property alias searchField: searchField
|
||||||
|
property alias clipboardListView: clipboardListView
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
focus: false
|
||||||
|
|
||||||
|
// Header
|
||||||
|
ClipboardHeader {
|
||||||
|
id: header
|
||||||
|
width: parent.width
|
||||||
|
totalCount: modal.totalCount
|
||||||
|
showKeyboardHints: modal.showKeyboardHints
|
||||||
|
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
||||||
|
onClearAllClicked: {
|
||||||
|
clearConfirmDialog.show("Clear All History?", "This will permanently delete all clipboard history.", function () {
|
||||||
|
modal.clearAll()
|
||||||
|
modal.hide()
|
||||||
|
}, function () {})
|
||||||
|
}
|
||||||
|
onCloseClicked: modal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search Field
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: ""
|
||||||
|
leftIconName: "search"
|
||||||
|
showClearButton: true
|
||||||
|
focus: true
|
||||||
|
ignoreLeftRightKeys: true
|
||||||
|
keyForwardTargets: [modal.modalFocusScope]
|
||||||
|
onTextChanged: {
|
||||||
|
modal.searchText = text
|
||||||
|
modal.updateFilteredModel()
|
||||||
|
}
|
||||||
|
Keys.onEscapePressed: function (event) {
|
||||||
|
modal.hide()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
Component.onCompleted: {
|
||||||
|
Qt.callLater(function () {
|
||||||
|
forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: modal
|
||||||
|
function onOpened() {
|
||||||
|
Qt.callLater(function () {
|
||||||
|
searchField.forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List Container
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - ClipboardConstants.headerHeight - 70
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
id: clipboardListView
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
model: filteredModel
|
||||||
|
|
||||||
|
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
interactive: true
|
||||||
|
flickDeceleration: 1500
|
||||||
|
maximumFlickVelocity: 2000
|
||||||
|
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||||
|
boundsMovement: Flickable.FollowBoundsBehavior
|
||||||
|
pressDelay: 0
|
||||||
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const itemHeight = ClipboardConstants.itemHeight + spacing
|
||||||
|
const itemY = index * itemHeight
|
||||||
|
const itemBottom = itemY + itemHeight
|
||||||
|
if (itemY < contentY) {
|
||||||
|
contentY = itemY
|
||||||
|
} else if (itemBottom > contentY + height) {
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && currentIndex >= 0) {
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "No clipboard entries found"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: filteredModel.count === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ClipboardEntry {
|
||||||
|
required property int index
|
||||||
|
required property var model
|
||||||
|
|
||||||
|
width: clipboardListView.width
|
||||||
|
height: ClipboardConstants.itemHeight
|
||||||
|
entryData: model.entry
|
||||||
|
entryIndex: index + 1
|
||||||
|
itemIndex: index
|
||||||
|
isSelected: clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
|
||||||
|
modal: clipboardContent.modal
|
||||||
|
listView: clipboardListView
|
||||||
|
onCopyRequested: clipboardContent.modal.copyEntry(model.entry)
|
||||||
|
onDeleteRequested: clipboardContent.modal.deleteEntry(model.entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer for keyboard hints
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 0
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard Hints Overlay
|
||||||
|
ClipboardKeyboardHints {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
visible: modal.showKeyboardHints
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals.Clipboard
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: entry
|
||||||
|
|
||||||
|
required property string entryData
|
||||||
|
required property int entryIndex
|
||||||
|
required property int itemIndex
|
||||||
|
required property bool isSelected
|
||||||
|
required property var modal
|
||||||
|
required property var listView
|
||||||
|
|
||||||
|
signal copyRequested
|
||||||
|
signal deleteRequested
|
||||||
|
|
||||||
|
readonly property string entryType: modal ? modal.getEntryType(entryData) : "text"
|
||||||
|
readonly property string entryPreview: modal ? modal.getEntryPreview(entryData) : entryData
|
||||||
|
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (isSelected) {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
|
||||||
|
}
|
||||||
|
return mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground
|
||||||
|
}
|
||||||
|
border.color: {
|
||||||
|
if (isSelected) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5)
|
||||||
|
}
|
||||||
|
return Theme.outlineStrong
|
||||||
|
}
|
||||||
|
border.width: isSelected ? 1.5 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
// Index indicator
|
||||||
|
Rectangle {
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
radius: 12
|
||||||
|
color: Theme.primarySelected
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: entryIndex.toString()
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content area
|
||||||
|
Row {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - 68
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
// Thumbnail/Icon
|
||||||
|
ClipboardThumbnail {
|
||||||
|
width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
|
||||||
|
height: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
entryData: entry.entryData
|
||||||
|
entryType: entry.entryType
|
||||||
|
modal: entry.modal
|
||||||
|
listView: entry.listView
|
||||||
|
itemIndex: entry.itemIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text content
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize) - Theme.spacingM
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
switch (entryType) {
|
||||||
|
case "image":
|
||||||
|
return "Image • " + entryPreview
|
||||||
|
case "long_text":
|
||||||
|
return "Long Text"
|
||||||
|
default:
|
||||||
|
return "Text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: entryPreview
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
maximumLineCount: entryType === "long_text" ? 3 : 1
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
DankActionButton {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 6
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: deleteRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click area
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: 40
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: copyRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals.Clipboard
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: header
|
||||||
|
|
||||||
|
property int totalCount: 0
|
||||||
|
property bool showKeyboardHints: false
|
||||||
|
|
||||||
|
signal keyboardHintsToggled
|
||||||
|
signal clearAllClicked
|
||||||
|
signal closeClicked
|
||||||
|
|
||||||
|
height: ClipboardConstants.headerHeight
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "content_paste"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: `Clipboard History (${totalCount})`
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "info"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
|
||||||
|
onClicked: keyboardHintsToggled()
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "delete_sweep"
|
||||||
|
iconSize: Theme.iconSize
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: clearAllClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: closeClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: clipboardHistoryModal
|
||||||
|
|
||||||
|
property int totalCount: 0
|
||||||
|
property var clipboardEntries: []
|
||||||
|
property string searchText: ""
|
||||||
|
property int selectedIndex: 0
|
||||||
|
property bool keyboardNavigationActive: false
|
||||||
|
property bool showKeyboardHints: false
|
||||||
|
property Component clipboardContent
|
||||||
|
property int activeImageLoads: 0
|
||||||
|
readonly property int maxConcurrentLoads: 3
|
||||||
|
|
||||||
|
function updateFilteredModel() {
|
||||||
|
filteredClipboardModel.clear()
|
||||||
|
for (var i = 0; i < clipboardModel.count; i++) {
|
||||||
|
const entry = clipboardModel.get(i).entry
|
||||||
|
if (searchText.trim().length === 0) {
|
||||||
|
filteredClipboardModel.append({
|
||||||
|
"entry": entry
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const content = getEntryPreview(entry).toLowerCase()
|
||||||
|
if (content.includes(searchText.toLowerCase())) {
|
||||||
|
filteredClipboardModel.append({
|
||||||
|
"entry": entry
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clipboardHistoryModal.totalCount = filteredClipboardModel.count
|
||||||
|
if (filteredClipboardModel.count === 0) {
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else if (selectedIndex >= filteredClipboardModel.count) {
|
||||||
|
selectedIndex = filteredClipboardModel.count - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
open()
|
||||||
|
clipboardHistoryModal.searchText = ""
|
||||||
|
clipboardHistoryModal.activeImageLoads = 0
|
||||||
|
refreshClipboard()
|
||||||
|
keyboardController.reset()
|
||||||
|
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (contentLoader.item && contentLoader.item.searchField) {
|
||||||
|
contentLoader.item.searchField.text = ""
|
||||||
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
close()
|
||||||
|
clipboardHistoryModal.searchText = ""
|
||||||
|
clipboardHistoryModal.activeImageLoads = 0
|
||||||
|
updateFilteredModel()
|
||||||
|
keyboardController.reset()
|
||||||
|
cleanupTempFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupTempFiles() {
|
||||||
|
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshClipboard() {
|
||||||
|
clipboardProcesses.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyEntry(entry) {
|
||||||
|
const entryId = entry.split('\t')[0]
|
||||||
|
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
|
||||||
|
ToastService.showInfo("Copied to clipboard")
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEntry(entry) {
|
||||||
|
clipboardProcesses.deleteEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
clipboardProcesses.clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntryPreview(entry) {
|
||||||
|
let content = entry.replace(/^\s*\d+\s+/, "")
|
||||||
|
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
|
||||||
|
const dimensionMatch = content.match(/(\d+)x(\d+)/)
|
||||||
|
if (dimensionMatch) {
|
||||||
|
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`
|
||||||
|
}
|
||||||
|
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i)
|
||||||
|
if (typeMatch) {
|
||||||
|
return `Image (${typeMatch[1].toUpperCase()})`
|
||||||
|
}
|
||||||
|
return "Image"
|
||||||
|
}
|
||||||
|
if (content.length > ClipboardConstants.previewLength) {
|
||||||
|
return content.substring(0, ClipboardConstants.previewLength) + "..."
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntryType(entry) {
|
||||||
|
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) {
|
||||||
|
return "image"
|
||||||
|
}
|
||||||
|
if (entry.length > ClipboardConstants.longTextThreshold) {
|
||||||
|
return "long_text"
|
||||||
|
}
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
width: ClipboardConstants.modalWidth
|
||||||
|
height: ClipboardConstants.modalHeight
|
||||||
|
backgroundColor: Theme.popupBackground()
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
borderColor: Theme.outlineMedium
|
||||||
|
borderWidth: 1
|
||||||
|
enableShadow: true
|
||||||
|
onBackgroundClicked: hide()
|
||||||
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
|
keyboardController.handleKey(event)
|
||||||
|
}
|
||||||
|
content: clipboardContent
|
||||||
|
|
||||||
|
ClipboardKeyboardController {
|
||||||
|
id: keyboardController
|
||||||
|
modal: clipboardHistoryModal
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmModal {
|
||||||
|
id: clearConfirmDialog
|
||||||
|
confirmButtonText: "Clear All"
|
||||||
|
confirmButtonColor: Theme.primary
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
clipboardHistoryModal.shouldHaveFocus = false
|
||||||
|
} else if (clipboardHistoryModal.shouldBeVisible) {
|
||||||
|
clipboardHistoryModal.shouldHaveFocus = true
|
||||||
|
clipboardHistoryModal.modalFocusScope.forceActiveFocus()
|
||||||
|
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
|
||||||
|
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property alias filteredClipboardModel: filteredClipboardModel
|
||||||
|
property alias clipboardModel: clipboardModel
|
||||||
|
property var confirmDialog: clearConfirmDialog
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: clipboardModel
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: filteredClipboardModel
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipboardProcesses {
|
||||||
|
id: clipboardProcesses
|
||||||
|
modal: clipboardHistoryModal
|
||||||
|
clipboardModel: clipboardModel
|
||||||
|
filteredClipboardModel: filteredClipboardModel
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
clipboardHistoryModal.show()
|
||||||
|
return "CLIPBOARD_OPEN_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
clipboardHistoryModal.hide()
|
||||||
|
return "CLIPBOARD_CLOSE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
clipboardHistoryModal.toggle()
|
||||||
|
return "CLIPBOARD_TOGGLE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "clipboard"
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboardContent: Component {
|
||||||
|
ClipboardContent {
|
||||||
|
modal: clipboardHistoryModal
|
||||||
|
filteredModel: filteredClipboardModel
|
||||||
|
clearConfirmDialog: clipboardHistoryModal.confirmDialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: keyboardController
|
||||||
|
|
||||||
|
required property var modal
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
modal.selectedIndex = 0
|
||||||
|
modal.keyboardNavigationActive = false
|
||||||
|
modal.showKeyboardHints = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNext() {
|
||||||
|
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
modal.keyboardNavigationActive = true
|
||||||
|
modal.selectedIndex = Math.min(modal.selectedIndex + 1, modal.filteredClipboardModel.count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrevious() {
|
||||||
|
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
modal.keyboardNavigationActive = true
|
||||||
|
modal.selectedIndex = Math.max(modal.selectedIndex - 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function copySelected() {
|
||||||
|
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
|
||||||
|
modal.copyEntry(selectedEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelected() {
|
||||||
|
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
|
||||||
|
modal.deleteEntry(selectedEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKey(event) {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (modal.keyboardNavigationActive) {
|
||||||
|
modal.keyboardNavigationActive = false
|
||||||
|
event.accepted = true
|
||||||
|
} else {
|
||||||
|
modal.hide()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
} else if (event.key === Qt.Key_Down) {
|
||||||
|
if (!modal.keyboardNavigationActive) {
|
||||||
|
modal.keyboardNavigationActive = true
|
||||||
|
modal.selectedIndex = 0
|
||||||
|
event.accepted = true
|
||||||
|
} else {
|
||||||
|
selectNext()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
} else if (event.key === Qt.Key_Up) {
|
||||||
|
if (!modal.keyboardNavigationActive) {
|
||||||
|
modal.keyboardNavigationActive = true
|
||||||
|
modal.selectedIndex = 0
|
||||||
|
event.accepted = true
|
||||||
|
} else if (modal.selectedIndex === 0) {
|
||||||
|
modal.keyboardNavigationActive = false
|
||||||
|
event.accepted = true
|
||||||
|
} else {
|
||||||
|
selectPrevious()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
} else if (event.key === Qt.Key_Delete && (event.modifiers & Qt.ShiftModifier)) {
|
||||||
|
modal.clearAll()
|
||||||
|
modal.hide()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (modal.keyboardNavigationActive) {
|
||||||
|
if ((event.key === Qt.Key_C && (event.modifiers & Qt.ControlModifier)) || event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
copySelected()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Delete) {
|
||||||
|
deleteSelected()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.key === Qt.Key_F10) {
|
||||||
|
modal.showKeyboardHints = !modal.showKeyboardHints
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals.Clipboard
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: keyboardHints
|
||||||
|
|
||||||
|
height: ClipboardConstants.keyboardHintsHeight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 2
|
||||||
|
opacity: visible ? 1 : 0
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Shift+Del: Clear All • Esc: Close"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: clipboardProcesses
|
||||||
|
|
||||||
|
required property var modal
|
||||||
|
required property var clipboardModel
|
||||||
|
required property var filteredClipboardModel
|
||||||
|
|
||||||
|
// Load clipboard entries
|
||||||
|
property var loadProcess: Process {
|
||||||
|
id: loadProcess
|
||||||
|
command: ["cliphist", "list"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
clipboardModel.clear()
|
||||||
|
const lines = text.trim().split('\n')
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim().length > 0) {
|
||||||
|
clipboardModel.append({
|
||||||
|
"entry": line
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modal.updateFilteredModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete single entry
|
||||||
|
property var deleteProcess: Process {
|
||||||
|
id: deleteProcess
|
||||||
|
property string deletedEntry: ""
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
for (var i = 0; i < clipboardModel.count; i++) {
|
||||||
|
if (clipboardModel.get(i).entry === deleteProcess.deletedEntry) {
|
||||||
|
clipboardModel.remove(i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var j = 0; j < filteredClipboardModel.count; j++) {
|
||||||
|
if (filteredClipboardModel.get(j).entry === deleteProcess.deletedEntry) {
|
||||||
|
filteredClipboardModel.remove(j)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modal.totalCount = filteredClipboardModel.count
|
||||||
|
if (filteredClipboardModel.count === 0) {
|
||||||
|
modal.keyboardNavigationActive = false
|
||||||
|
modal.selectedIndex = 0
|
||||||
|
} else if (modal.selectedIndex >= filteredClipboardModel.count) {
|
||||||
|
modal.selectedIndex = filteredClipboardModel.count - 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Failed to delete clipboard entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all entries
|
||||||
|
property var clearProcess: Process {
|
||||||
|
id: clearProcess
|
||||||
|
command: ["cliphist", "wipe"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
clipboardModel.clear()
|
||||||
|
filteredClipboardModel.clear()
|
||||||
|
modal.totalCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
loadProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEntry(entry) {
|
||||||
|
deleteProcess.deletedEntry = entry
|
||||||
|
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(/'/g, "'\\''")}' | cliphist delete`]
|
||||||
|
deleteProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
clearProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals.Clipboard
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: thumbnail
|
||||||
|
|
||||||
|
required property string entryData
|
||||||
|
required property string entryType
|
||||||
|
required property var modal
|
||||||
|
required property var listView
|
||||||
|
required property int itemIndex
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: thumbnailImage
|
||||||
|
|
||||||
|
property string entryId: entryData.split('\t')[0]
|
||||||
|
property bool isVisible: false
|
||||||
|
property string cachedImageData: ""
|
||||||
|
property bool loadQueued: false
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: ""
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
smooth: true
|
||||||
|
cache: false
|
||||||
|
visible: false
|
||||||
|
asynchronous: true
|
||||||
|
sourceSize.width: 128
|
||||||
|
sourceSize.height: 128
|
||||||
|
|
||||||
|
onCachedImageDataChanged: {
|
||||||
|
if (cachedImageData) {
|
||||||
|
source = ""
|
||||||
|
source = `data:image/png;base64,${cachedImageData}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryLoadImage() {
|
||||||
|
if (!loadQueued && entryType === "image" && !cachedImageData) {
|
||||||
|
loadQueued = true
|
||||||
|
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
||||||
|
modal.activeImageLoads++
|
||||||
|
imageLoader.running = true
|
||||||
|
} else {
|
||||||
|
retryTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: retryTimer
|
||||||
|
interval: ClipboardConstants.retryInterval
|
||||||
|
onTriggered: {
|
||||||
|
if (thumbnailImage.loadQueued && !imageLoader.running) {
|
||||||
|
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
||||||
|
modal.activeImageLoads++
|
||||||
|
imageLoader.running = true
|
||||||
|
} else {
|
||||||
|
retryTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (entryType !== "image") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if item is visible on screen initially
|
||||||
|
const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing)
|
||||||
|
const viewTop = listView.contentY
|
||||||
|
const viewBottom = viewTop + listView.height
|
||||||
|
isVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom)
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
tryLoadImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: listView
|
||||||
|
function onContentYChanged() {
|
||||||
|
if (entryType !== "image") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing)
|
||||||
|
const viewTop = listView.contentY - ClipboardConstants.viewportBuffer
|
||||||
|
const viewBottom = viewTop + listView.height + ClipboardConstants.extendedBuffer
|
||||||
|
const nowVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom)
|
||||||
|
|
||||||
|
if (nowVisible && !thumbnailImage.isVisible) {
|
||||||
|
thumbnailImage.isVisible = true
|
||||||
|
thumbnailImage.tryLoadImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: imageLoader
|
||||||
|
running: false
|
||||||
|
command: ["sh", "-c", `cliphist decode ${thumbnailImage.entryId} | base64 -w 0`]
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const imageData = text.trim()
|
||||||
|
if (imageData && imageData.length > 0) {
|
||||||
|
thumbnailImage.cachedImageData = imageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
thumbnailImage.loadQueued = false
|
||||||
|
if (modal.activeImageLoads > 0) {
|
||||||
|
modal.activeImageLoads--
|
||||||
|
}
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.warn("Failed to load clipboard image:", thumbnailImage.entryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rounded mask effect for images
|
||||||
|
MultiEffect {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
source: thumbnailImage
|
||||||
|
maskEnabled: true
|
||||||
|
maskSource: clipboardCircularMask
|
||||||
|
visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != ""
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: clipboardCircularMask
|
||||||
|
width: ClipboardConstants.thumbnailSize - 4
|
||||||
|
height: ClipboardConstants.thumbnailSize - 4
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: "black"
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback icon
|
||||||
|
DankIcon {
|
||||||
|
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
|
||||||
|
name: {
|
||||||
|
if (entryType === "image") {
|
||||||
|
return "image"
|
||||||
|
}
|
||||||
|
if (entryType === "long_text") {
|
||||||
|
return "subject"
|
||||||
|
}
|
||||||
|
return "content_copy"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
}
|
||||||
227
quickshell/.config/quickshell/Modals/Common/ConfirmModal.qml
Normal file
227
quickshell/.config/quickshell/Modals/Common/ConfirmModal.qml
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string confirmTitle: ""
|
||||||
|
property string confirmMessage: ""
|
||||||
|
property string confirmButtonText: "Confirm"
|
||||||
|
property string cancelButtonText: "Cancel"
|
||||||
|
property color confirmButtonColor: Theme.primary
|
||||||
|
property var onConfirm: function () {}
|
||||||
|
property var onCancel: function () {}
|
||||||
|
property int selectedButton: -1
|
||||||
|
property bool keyboardNavigation: false
|
||||||
|
|
||||||
|
function show(title, message, onConfirmCallback, onCancelCallback) {
|
||||||
|
confirmTitle = title || ""
|
||||||
|
confirmMessage = message || ""
|
||||||
|
confirmButtonText = "Confirm"
|
||||||
|
cancelButtonText = "Cancel"
|
||||||
|
confirmButtonColor = Theme.primary
|
||||||
|
onConfirm = onConfirmCallback || (() => {})
|
||||||
|
onCancel = onCancelCallback || (() => {})
|
||||||
|
selectedButton = -1
|
||||||
|
keyboardNavigation = false
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWithOptions(options) {
|
||||||
|
confirmTitle = options.title || ""
|
||||||
|
confirmMessage = options.message || ""
|
||||||
|
confirmButtonText = options.confirmText || "Confirm"
|
||||||
|
cancelButtonText = options.cancelText || "Cancel"
|
||||||
|
confirmButtonColor = options.confirmColor || Theme.primary
|
||||||
|
onConfirm = options.onConfirm || (() => {})
|
||||||
|
onCancel = options.onCancel || (() => {})
|
||||||
|
selectedButton = -1
|
||||||
|
keyboardNavigation = false
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectButton() {
|
||||||
|
close()
|
||||||
|
if (selectedButton === 0) {
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (onConfirm) {
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: false
|
||||||
|
allowStacking: true
|
||||||
|
width: 350
|
||||||
|
height: 160
|
||||||
|
enableShadow: true
|
||||||
|
shouldHaveFocus: true
|
||||||
|
onBackgroundClicked: {
|
||||||
|
close()
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onOpened: {
|
||||||
|
modalFocusScope.forceActiveFocus()
|
||||||
|
modalFocusScope.focus = true
|
||||||
|
shouldHaveFocus = true
|
||||||
|
}
|
||||||
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Escape:
|
||||||
|
close()
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Left:
|
||||||
|
case Qt.Key_Up:
|
||||||
|
keyboardNavigation = true
|
||||||
|
selectedButton = 0
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Right:
|
||||||
|
case Qt.Key_Down:
|
||||||
|
keyboardNavigation = true
|
||||||
|
selectedButton = 1
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
keyboardNavigation = true
|
||||||
|
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
if (selectedButton !== -1) {
|
||||||
|
selectButton()
|
||||||
|
} else {
|
||||||
|
selectedButton = 1
|
||||||
|
selectButton()
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: confirmTitle
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: confirmMessage
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
height: Theme.spacingS
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 120
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (keyboardNavigation && selectedButton === 0) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||||
|
} else if (cancelButton.containsMouse) {
|
||||||
|
return Theme.surfacePressed
|
||||||
|
} else {
|
||||||
|
return Theme.surfaceVariantAlpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
|
||||||
|
border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: cancelButtonText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cancelButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
selectedButton = 0
|
||||||
|
selectButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 120
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
const baseColor = confirmButtonColor
|
||||||
|
if (keyboardNavigation && selectedButton === 1) {
|
||||||
|
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1)
|
||||||
|
} else if (confirmButton.containsMouse) {
|
||||||
|
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9)
|
||||||
|
} else {
|
||||||
|
return baseColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
|
||||||
|
border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: confirmButtonText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.primaryText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: confirmButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
selectedButton = 1
|
||||||
|
selectButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
234
quickshell/.config/quickshell/Modals/Common/DankModal.qml
Normal file
234
quickshell/.config/quickshell/Modals/Common/DankModal.qml
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "quickshell:modal"
|
||||||
|
|
||||||
|
property alias content: contentLoader.sourceComponent
|
||||||
|
property alias contentLoader: contentLoader
|
||||||
|
property real width: 400
|
||||||
|
property real height: 300
|
||||||
|
readonly property real screenWidth: screen ? screen.width : 1920
|
||||||
|
readonly property real screenHeight: screen ? screen.height : 1080
|
||||||
|
property bool showBackground: true
|
||||||
|
property real backgroundOpacity: 0.5
|
||||||
|
property string positioning: "center"
|
||||||
|
property point customPosition: Qt.point(0, 0)
|
||||||
|
property bool closeOnEscapeKey: true
|
||||||
|
property bool closeOnBackgroundClick: true
|
||||||
|
property string animationType: "scale"
|
||||||
|
property int animationDuration: Theme.shorterDuration
|
||||||
|
property var animationEasing: Theme.emphasizedEasing
|
||||||
|
property color backgroundColor: Theme.surfaceContainer
|
||||||
|
property color borderColor: Theme.outlineMedium
|
||||||
|
property real borderWidth: 1
|
||||||
|
property real cornerRadius: Theme.cornerRadius
|
||||||
|
property bool enableShadow: false
|
||||||
|
property alias modalFocusScope: focusScope
|
||||||
|
property bool shouldBeVisible: false
|
||||||
|
property bool shouldHaveFocus: shouldBeVisible
|
||||||
|
property bool allowFocusOverride: false
|
||||||
|
property bool allowStacking: false
|
||||||
|
|
||||||
|
signal opened
|
||||||
|
signal dialogClosed
|
||||||
|
signal backgroundClicked
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
ModalManager.openModal(root)
|
||||||
|
closeTimer.stop()
|
||||||
|
shouldBeVisible = true
|
||||||
|
visible = true
|
||||||
|
focusScope.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
shouldBeVisible = false
|
||||||
|
closeTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: shouldBeVisible
|
||||||
|
color: "transparent"
|
||||||
|
WlrLayershell.layer: WlrLayershell.Top // if set to overlay -> virtual keyboards can be stuck under modal
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (root.visible) {
|
||||||
|
opened()
|
||||||
|
} else {
|
||||||
|
if (Qt.inputMethod) {
|
||||||
|
Qt.inputMethod.hide()
|
||||||
|
Qt.inputMethod.reset()
|
||||||
|
}
|
||||||
|
dialogClosed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCloseAllModalsExcept(excludedModal) {
|
||||||
|
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: ModalManager
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: closeTimer
|
||||||
|
|
||||||
|
interval: animationDuration + 50
|
||||||
|
onTriggered: {
|
||||||
|
visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: background
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "black"
|
||||||
|
opacity: root.showBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||||
|
visible: root.showBackground
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: root.closeOnBackgroundClick
|
||||||
|
onClicked: mouse => {
|
||||||
|
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
|
||||||
|
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
|
||||||
|
root.backgroundClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root.animationDuration
|
||||||
|
easing.type: root.animationEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: contentContainer
|
||||||
|
|
||||||
|
width: root.width
|
||||||
|
height: root.height
|
||||||
|
anchors.centerIn: positioning === "center" ? parent : undefined
|
||||||
|
x: {
|
||||||
|
if (positioning === "top-right") {
|
||||||
|
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL)
|
||||||
|
} else if (positioning === "custom") {
|
||||||
|
return root.customPosition.x
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
y: {
|
||||||
|
if (positioning === "top-right") {
|
||||||
|
return Theme.barHeight + Theme.spacingXS
|
||||||
|
} else if (positioning === "custom") {
|
||||||
|
return root.customPosition.y
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
color: root.backgroundColor
|
||||||
|
radius: root.cornerRadius
|
||||||
|
border.color: root.borderColor
|
||||||
|
border.width: root.borderWidth
|
||||||
|
layer.enabled: root.enableShadow
|
||||||
|
opacity: root.shouldBeVisible ? 1 : 0
|
||||||
|
scale: root.animationType === "scale" ? (root.shouldBeVisible ? 1 : 0.9) : 1
|
||||||
|
transform: root.animationType === "slide" ? slideTransform : null
|
||||||
|
|
||||||
|
Translate {
|
||||||
|
id: slideTransform
|
||||||
|
|
||||||
|
x: root.shouldBeVisible ? 0 : 15
|
||||||
|
y: root.shouldBeVisible ? 0 : -30
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: contentLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.visible
|
||||||
|
asynchronous: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root.animationDuration
|
||||||
|
easing.type: root.animationEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
enabled: root.animationType === "scale"
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root.animationDuration
|
||||||
|
easing.type: root.animationEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
shadowEnabled: true
|
||||||
|
shadowHorizontalOffset: 0
|
||||||
|
shadowVerticalOffset: 8
|
||||||
|
shadowBlur: 1
|
||||||
|
shadowColor: Theme.shadowStrong
|
||||||
|
shadowOpacity: 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: focusScope
|
||||||
|
|
||||||
|
objectName: "modalFocusScope"
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.visible // Only active when the modal is visible
|
||||||
|
focus: root.visible
|
||||||
|
Keys.onEscapePressed: event => {
|
||||||
|
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
||||||
|
root.close()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible && shouldHaveFocus) {
|
||||||
|
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onShouldHaveFocusChanged() {
|
||||||
|
if (shouldHaveFocus && visible) {
|
||||||
|
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,818 @@
|
||||||
|
import Qt.labs.folderlistmodel
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: fileBrowserModal
|
||||||
|
|
||||||
|
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||||
|
property string currentPath: ""
|
||||||
|
property var fileExtensions: ["*.*"]
|
||||||
|
property alias filterExtensions: fileBrowserModal.fileExtensions
|
||||||
|
property string browserTitle: "Select File"
|
||||||
|
property string browserIcon: "folder_open"
|
||||||
|
property string browserType: "generic" // "wallpaper" or "profile" for last path memory
|
||||||
|
property bool showHiddenFiles: false
|
||||||
|
property int selectedIndex: -1
|
||||||
|
property bool keyboardNavigationActive: false
|
||||||
|
property bool backButtonFocused: false
|
||||||
|
property bool saveMode: false // Enable save functionality
|
||||||
|
property string defaultFileName: "" // Default filename for save mode
|
||||||
|
property int keyboardSelectionIndex: -1
|
||||||
|
property bool keyboardSelectionRequested: false
|
||||||
|
property bool showKeyboardHints: false
|
||||||
|
property bool showFileInfo: false
|
||||||
|
property string selectedFilePath: ""
|
||||||
|
property string selectedFileName: ""
|
||||||
|
property bool selectedFileIsDir: false
|
||||||
|
property bool showOverwriteConfirmation: false
|
||||||
|
property string pendingFilePath: ""
|
||||||
|
|
||||||
|
signal fileSelected(string path)
|
||||||
|
|
||||||
|
function isImageFile(fileName) {
|
||||||
|
if (!fileName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const ext = fileName.toLowerCase().split('.').pop()
|
||||||
|
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastPath() {
|
||||||
|
const lastPath = browserType === "wallpaper" ? SessionData.wallpaperLastPath : browserType === "profile" ? SessionData.profileLastPath : ""
|
||||||
|
return (lastPath && lastPath !== "") ? lastPath : homeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLastPath(path) {
|
||||||
|
if (browserType === "wallpaper") {
|
||||||
|
SessionData.setWallpaperLastPath(path)
|
||||||
|
} else if (browserType === "profile") {
|
||||||
|
SessionData.setProfileLastPath(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedFileData(path, name, isDir) {
|
||||||
|
selectedFilePath = path
|
||||||
|
selectedFileName = name
|
||||||
|
selectedFileIsDir = isDir
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateUp() {
|
||||||
|
const path = currentPath
|
||||||
|
if (path === homeDir)
|
||||||
|
return
|
||||||
|
|
||||||
|
const lastSlash = path.lastIndexOf('/')
|
||||||
|
if (lastSlash > 0) {
|
||||||
|
const newPath = path.substring(0, lastSlash)
|
||||||
|
if (newPath.length < homeDir.length) {
|
||||||
|
currentPath = homeDir
|
||||||
|
saveLastPath(homeDir)
|
||||||
|
} else {
|
||||||
|
currentPath = newPath
|
||||||
|
saveLastPath(newPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(path) {
|
||||||
|
currentPath = path
|
||||||
|
saveLastPath(path)
|
||||||
|
selectedIndex = -1
|
||||||
|
backButtonFocused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyboardFileSelection(index) {
|
||||||
|
if (index >= 0) {
|
||||||
|
keyboardSelectionTimer.targetIndex = index
|
||||||
|
keyboardSelectionTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeKeyboardSelection(index) {
|
||||||
|
keyboardSelectionIndex = index
|
||||||
|
keyboardSelectionRequested = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSaveFile(filePath) {
|
||||||
|
// Ensure the filePath has the correct file:// protocol format
|
||||||
|
var normalizedPath = filePath
|
||||||
|
if (!normalizedPath.startsWith("file://")) {
|
||||||
|
normalizedPath = "file://" + filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists by looking through the folder model
|
||||||
|
var exists = false
|
||||||
|
var fileName = filePath.split('/').pop()
|
||||||
|
|
||||||
|
for (var i = 0; i < folderModel.count; i++) {
|
||||||
|
if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
pendingFilePath = normalizedPath
|
||||||
|
showOverwriteConfirmation = true
|
||||||
|
} else {
|
||||||
|
fileSelected(normalizedPath)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objectName: "fileBrowserModal"
|
||||||
|
allowStacking: true
|
||||||
|
Component.onCompleted: {
|
||||||
|
currentPath = getLastPath()
|
||||||
|
}
|
||||||
|
width: 800
|
||||||
|
height: 600
|
||||||
|
enableShadow: true
|
||||||
|
visible: false
|
||||||
|
onBackgroundClicked: close()
|
||||||
|
onOpened: {
|
||||||
|
modalFocusScope.forceActiveFocus()
|
||||||
|
}
|
||||||
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
|
keyboardController.handleKey(event)
|
||||||
|
}
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
currentPath = getLastPath()
|
||||||
|
selectedIndex = -1
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
backButtonFocused = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCurrentPathChanged: {
|
||||||
|
selectedFilePath = ""
|
||||||
|
selectedFileName = ""
|
||||||
|
selectedFileIsDir = false
|
||||||
|
}
|
||||||
|
onSelectedIndexChanged: {
|
||||||
|
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
|
||||||
|
selectedFilePath = ""
|
||||||
|
selectedFileName = ""
|
||||||
|
selectedFileIsDir = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderListModel {
|
||||||
|
id: folderModel
|
||||||
|
|
||||||
|
showDirsFirst: true
|
||||||
|
showDotAndDotDot: false
|
||||||
|
showHidden: fileBrowserModal.showHiddenFiles
|
||||||
|
nameFilters: fileExtensions
|
||||||
|
showFiles: true
|
||||||
|
showDirs: true
|
||||||
|
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: keyboardController
|
||||||
|
|
||||||
|
property int totalItems: folderModel.count
|
||||||
|
property int gridColumns: 5
|
||||||
|
|
||||||
|
function handleKey(event) {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
close()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// F10 toggles keyboard hints
|
||||||
|
if (event.key === Qt.Key_F10) {
|
||||||
|
showKeyboardHints = !showKeyboardHints
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// F1 or I key for file information
|
||||||
|
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
|
||||||
|
showFileInfo = !showFileInfo
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Alt+Left or Backspace to go back
|
||||||
|
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
|
||||||
|
if (currentPath !== homeDir) {
|
||||||
|
navigateUp()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!keyboardNavigationActive) {
|
||||||
|
if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right) {
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
} else {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else if (selectedIndex < totalItems - 1) {
|
||||||
|
selectedIndex++
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
} else {
|
||||||
|
selectedIndex = 0
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Backtab:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = totalItems - 1
|
||||||
|
} else if (selectedIndex > 0) {
|
||||||
|
selectedIndex--
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
} else {
|
||||||
|
selectedIndex = totalItems - 1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Left:
|
||||||
|
if (backButtonFocused)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (selectedIndex > 0) {
|
||||||
|
selectedIndex--
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Right:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else if (selectedIndex < totalItems - 1) {
|
||||||
|
selectedIndex++
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Up:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
// Go to first row, appropriate column
|
||||||
|
var col = selectedIndex % gridColumns
|
||||||
|
selectedIndex = Math.min(col, totalItems - 1)
|
||||||
|
} else if (selectedIndex >= gridColumns) {
|
||||||
|
// Move up one row
|
||||||
|
selectedIndex -= gridColumns
|
||||||
|
} else if (currentPath !== homeDir) {
|
||||||
|
// At top row, go to back button
|
||||||
|
backButtonFocused = true
|
||||||
|
selectedIndex = -1
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Down:
|
||||||
|
if (backButtonFocused) {
|
||||||
|
backButtonFocused = false
|
||||||
|
selectedIndex = 0
|
||||||
|
} else {
|
||||||
|
// Move down one row if possible
|
||||||
|
var newIndex = selectedIndex + gridColumns
|
||||||
|
if (newIndex < totalItems) {
|
||||||
|
selectedIndex = newIndex
|
||||||
|
} else {
|
||||||
|
// If can't go down a full row, go to last item in the column if exists
|
||||||
|
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns
|
||||||
|
var col = selectedIndex % gridColumns
|
||||||
|
var targetIndex = lastRowStart + col
|
||||||
|
if (targetIndex < totalItems && targetIndex > selectedIndex) {
|
||||||
|
selectedIndex = targetIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
case Qt.Key_Space:
|
||||||
|
if (backButtonFocused)
|
||||||
|
navigateUp()
|
||||||
|
else if (selectedIndex >= 0 && selectedIndex < totalItems)
|
||||||
|
// Trigger selection by setting the grid's current index and using signal
|
||||||
|
fileBrowserModal.keyboardFileSelection(selectedIndex)
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: keyboardSelectionTimer
|
||||||
|
|
||||||
|
property int targetIndex: -1
|
||||||
|
|
||||||
|
interval: 1
|
||||||
|
onTriggered: {
|
||||||
|
// Access the currently selected item through model role names
|
||||||
|
// This will work because QML models expose role data
|
||||||
|
executeKeyboardSelection(targetIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: browserIcon
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: browserTitle
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "info"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: fileBrowserModal.showKeyboardHints = !fileBrowserModal.showKeyboardHints
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: fileBrowserModal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
|
||||||
|
opacity: currentPath !== homeDir ? 1 : 0
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "arrow_back"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: backButtonMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: currentPath !== homeDir
|
||||||
|
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
enabled: currentPath !== homeDir
|
||||||
|
onClicked: navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: fileBrowserModal.currentPath.replace("file://", "")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width - 40 - Theme.spacingS
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
maximumLineCount: 1
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankGridView {
|
||||||
|
id: fileGrid
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 80
|
||||||
|
clip: true
|
||||||
|
cellWidth: 150
|
||||||
|
cellHeight: 130
|
||||||
|
cacheBuffer: 260
|
||||||
|
model: folderModel
|
||||||
|
currentIndex: selectedIndex
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive && currentIndex >= 0)
|
||||||
|
positionViewAtIndex(currentIndex, GridView.Contain)
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.horizontal: ScrollBar {
|
||||||
|
policy: ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: StyledRect {
|
||||||
|
id: delegateRoot
|
||||||
|
|
||||||
|
required property bool fileIsDir
|
||||||
|
required property string filePath
|
||||||
|
required property string fileName
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: 140
|
||||||
|
height: 120
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
||||||
|
return Theme.surfacePressed
|
||||||
|
|
||||||
|
return mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
}
|
||||||
|
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.outline
|
||||||
|
border.width: (mouseArea.containsMouse || (keyboardNavigationActive && delegateRoot.index === selectedIndex)) ? 1 : 0
|
||||||
|
// Update file info when this item gets selected via keyboard or initially
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for selectedIndex changes to update file info during keyboard navigation
|
||||||
|
Connections {
|
||||||
|
function onSelectedIndexChanged() {
|
||||||
|
if (keyboardNavigationActive && selectedIndex === delegateRoot.index)
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
target: fileBrowserModal
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 80
|
||||||
|
height: 60
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
CachingImage {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : ""
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
visible: !delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)
|
||||||
|
maxCacheSize: 80
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "description"
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.primary
|
||||||
|
visible: !delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "folder"
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.primary
|
||||||
|
visible: delegateRoot.fileIsDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: delegateRoot.fileName || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: 120
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
// Update selected file info and index first
|
||||||
|
selectedIndex = delegateRoot.index
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
if (delegateRoot.fileIsDir) {
|
||||||
|
navigateTo(delegateRoot.filePath)
|
||||||
|
} else {
|
||||||
|
fileSelected(delegateRoot.filePath)
|
||||||
|
fileBrowserModal.close() // Close modal after file selection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle keyboard selection
|
||||||
|
Connections {
|
||||||
|
function onKeyboardSelectionRequestedChanged() {
|
||||||
|
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === delegateRoot.index) {
|
||||||
|
fileBrowserModal.keyboardSelectionRequested = false
|
||||||
|
selectedIndex = delegateRoot.index
|
||||||
|
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
|
||||||
|
if (delegateRoot.fileIsDir) {
|
||||||
|
navigateTo(delegateRoot.filePath)
|
||||||
|
} else {
|
||||||
|
fileSelected(delegateRoot.filePath)
|
||||||
|
fileBrowserModal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: fileBrowserModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveRow
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
height: saveMode ? 40 : 0
|
||||||
|
visible: saveMode
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: fileNameInput
|
||||||
|
|
||||||
|
width: parent.width - saveButton.width - Theme.spacingM
|
||||||
|
height: 40
|
||||||
|
text: defaultFileName
|
||||||
|
placeholderText: "Enter filename..."
|
||||||
|
ignoreLeftRightKeys: false
|
||||||
|
focus: saveMode
|
||||||
|
topPadding: Theme.spacingS
|
||||||
|
bottomPadding: Theme.spacingS
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (saveMode)
|
||||||
|
Qt.callLater(() => {
|
||||||
|
forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onAccepted: {
|
||||||
|
if (text.trim() !== "") {
|
||||||
|
// Remove file:// protocol from currentPath if present for proper construction
|
||||||
|
var basePath = currentPath.replace(/^file:\/\//, '')
|
||||||
|
var fullPath = basePath + "/" + text.trim()
|
||||||
|
// Ensure consistent path format - remove any double slashes and normalize
|
||||||
|
fullPath = fullPath.replace(/\/+/g, '/')
|
||||||
|
handleSaveFile(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
id: saveButton
|
||||||
|
|
||||||
|
width: 80
|
||||||
|
height: 40
|
||||||
|
color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Save"
|
||||||
|
color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
stateColor: Theme.primary
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
enabled: fileNameInput.text.trim() !== ""
|
||||||
|
onClicked: {
|
||||||
|
if (fileNameInput.text.trim() !== "") {
|
||||||
|
// Remove file:// protocol from currentPath if present for proper construction
|
||||||
|
var basePath = currentPath.replace(/^file:\/\//, '')
|
||||||
|
var fullPath = basePath + "/" + fileNameInput.text.trim()
|
||||||
|
// Ensure consistent path format - remove any double slashes and normalize
|
||||||
|
fullPath = fullPath.replace(/\/+/g, '/')
|
||||||
|
handleSaveFile(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardHints {
|
||||||
|
id: keyboardHints
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
showHints: fileBrowserModal.showKeyboardHints
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInfo {
|
||||||
|
id: fileInfo
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
width: 300
|
||||||
|
showFileInfo: fileBrowserModal.showFileInfo
|
||||||
|
selectedIndex: fileBrowserModal.selectedIndex
|
||||||
|
sourceFolderModel: folderModel
|
||||||
|
currentPath: fileBrowserModal.currentPath
|
||||||
|
currentFileName: fileBrowserModal.selectedFileName
|
||||||
|
currentFileIsDir: fileBrowserModal.selectedFileIsDir
|
||||||
|
currentFileExtension: {
|
||||||
|
if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.')
|
||||||
|
return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite confirmation dialog
|
||||||
|
Item {
|
||||||
|
id: overwriteDialog
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: showOverwriteConfirmation
|
||||||
|
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
pendingFilePath = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
fileSelected(pendingFilePath)
|
||||||
|
pendingFilePath = ""
|
||||||
|
Qt.callLater(() => fileBrowserModal.close())
|
||||||
|
}
|
||||||
|
|
||||||
|
focus: showOverwriteConfirmation
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.shadowStrong
|
||||||
|
opacity: 0.8
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
pendingFilePath = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 400
|
||||||
|
height: 160
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingL * 2
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("File Already Exists")
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("A file with this name already exists. Do you want to overwrite it?")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: 80
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cancelArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
pendingFilePath = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: 90
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("Overwrite")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.background
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: overwriteArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
showOverwriteConfirmation = false
|
||||||
|
fileSelected(pendingFilePath)
|
||||||
|
pendingFilePath = ""
|
||||||
|
Qt.callLater(() => fileBrowserModal.close())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
237
quickshell/.config/quickshell/Modals/FileBrowser/FileInfo.qml
Normal file
237
quickshell/.config/quickshell/Modals/FileBrowser/FileInfo.qml
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtCore
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool showFileInfo: false
|
||||||
|
property int selectedIndex: -1
|
||||||
|
property var sourceFolderModel: null
|
||||||
|
property string currentPath: ""
|
||||||
|
|
||||||
|
height: 200
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
||||||
|
border.color: Theme.secondary
|
||||||
|
border.width: 2
|
||||||
|
opacity: showFileInfo ? 1 : 0
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
onShowFileInfoChanged: {
|
||||||
|
if (showFileInfo && currentFileName && currentPath) {
|
||||||
|
const fullPath = currentPath + "/" + currentFileName
|
||||||
|
fileStatProcess.selectedFilePath = fullPath
|
||||||
|
fileStatProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: fileStatProcess
|
||||||
|
command: ["stat", "-c", "%y|%A|%s|%n", selectedFilePath]
|
||||||
|
property string selectedFilePath: ""
|
||||||
|
property var fileStats: null
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text && text.trim()) {
|
||||||
|
const parts = text.trim().split('|')
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
fileStatProcess.fileStats = {
|
||||||
|
"modifiedTime": parts[0],
|
||||||
|
"permissions": parts[1],
|
||||||
|
"size": parseInt(parts[2]) || 0,
|
||||||
|
"fullPath": parts[3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: function (exitCode) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
property string currentFileName: ""
|
||||||
|
property bool currentFileIsDir: false
|
||||||
|
property string currentFileExtension: ""
|
||||||
|
|
||||||
|
onCurrentFileNameChanged: {
|
||||||
|
if (showFileInfo && currentFileName && currentPath) {
|
||||||
|
const fullPath = currentPath + "/" + currentFileName
|
||||||
|
if (fullPath !== fileStatProcess.selectedFilePath) {
|
||||||
|
fileStatProcess.selectedFilePath = fullPath
|
||||||
|
fileStatProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFileInfo(filePath, fileName, isDirectory) {
|
||||||
|
if (filePath && filePath !== fileStatProcess.selectedFilePath) {
|
||||||
|
fileStatProcess.selectedFilePath = filePath
|
||||||
|
currentFileName = fileName || ""
|
||||||
|
currentFileIsDir = isDirectory || false
|
||||||
|
|
||||||
|
let ext = ""
|
||||||
|
if (!isDirectory && fileName) {
|
||||||
|
const lastDot = fileName.lastIndexOf('.')
|
||||||
|
if (lastDot > 0) {
|
||||||
|
ext = fileName.substring(lastDot + 1).toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentFileExtension = ext
|
||||||
|
|
||||||
|
if (showFileInfo) {
|
||||||
|
fileStatProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var currentFileDisplayData: {
|
||||||
|
if (selectedIndex < 0 || !sourceFolderModel) {
|
||||||
|
return {
|
||||||
|
"exists": false,
|
||||||
|
"name": "No selection",
|
||||||
|
"type": "",
|
||||||
|
"size": "",
|
||||||
|
"modified": "",
|
||||||
|
"permissions": "",
|
||||||
|
"extension": "",
|
||||||
|
"position": "N/A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasValidFile = currentFileName !== ""
|
||||||
|
return {
|
||||||
|
"exists": hasValidFile,
|
||||||
|
"name": hasValidFile ? currentFileName : "Loading...",
|
||||||
|
"type": currentFileIsDir ? "Directory" : "File",
|
||||||
|
"size": fileStatProcess.fileStats ? formatFileSize(fileStatProcess.fileStats.size) : "Calculating...",
|
||||||
|
"modified": fileStatProcess.fileStats ? formatDateTime(fileStatProcess.fileStats.modifiedTime) : "Loading...",
|
||||||
|
"permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...",
|
||||||
|
"extension": currentFileExtension,
|
||||||
|
"position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "info"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.secondary
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "File Information"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.name
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.type + (currentFileDisplayData.extension ? " (." + currentFileDisplayData.extension + ")" : "")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.size
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
visible: currentFileDisplayData.exists && !currentFileIsDir
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.modified
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: currentFileDisplayData.exists
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.permissions
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
visible: currentFileDisplayData.exists
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: currentFileDisplayData.position
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "F1/I: Toggle • F10: Help"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes) {
|
||||||
|
if (bytes === 0 || !bytes) {
|
||||||
|
return "0 B"
|
||||||
|
}
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(dateTimeString) {
|
||||||
|
if (!dateTimeString) {
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
const parts = dateTimeString.split(' ')
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
return parts[0] + " " + parts[1].split('.')[0]
|
||||||
|
}
|
||||||
|
return dateTimeString
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool showHints: false
|
||||||
|
|
||||||
|
height: 80
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 2
|
||||||
|
opacity: showHints ? 1 : 0
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
162
quickshell/.config/quickshell/Modals/NetworkInfoModal.qml
Normal file
162
quickshell/.config/quickshell/Modals/NetworkInfoModal.qml
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool networkInfoModalVisible: false
|
||||||
|
property string networkSSID: ""
|
||||||
|
property var networkData: null
|
||||||
|
|
||||||
|
function showNetworkInfo(ssid, data) {
|
||||||
|
networkSSID = ssid
|
||||||
|
networkData = data
|
||||||
|
networkInfoModalVisible = true
|
||||||
|
open()
|
||||||
|
NetworkService.fetchNetworkInfo(ssid)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDialog() {
|
||||||
|
networkInfoModalVisible = false
|
||||||
|
close()
|
||||||
|
networkSSID = ""
|
||||||
|
networkData = null
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: networkInfoModalVisible
|
||||||
|
width: 600
|
||||||
|
height: 500
|
||||||
|
enableShadow: true
|
||||||
|
onBackgroundClicked: hideDialog()
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible) {
|
||||||
|
networkSSID = ""
|
||||||
|
networkData = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - 40
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Network Information"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: `Details for "${networkSSID}"`
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: root.hideDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: detailsRect
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 140
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceHover
|
||||||
|
border.color: Theme.outlineStrong
|
||||||
|
border.width: 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
contentHeight: detailsText.contentHeight
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: detailsText
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: closeText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Close"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.background
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: closeArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.hideDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
146
quickshell/.config/quickshell/Modals/NotificationModal.qml
Normal file
146
quickshell/.config/quickshell/Modals/NotificationModal.qml
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Modules.Notifications.Center
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: notificationModal
|
||||||
|
|
||||||
|
property bool notificationModalOpen: false
|
||||||
|
property var notificationListRef: null
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
notificationModalOpen = true
|
||||||
|
NotificationService.onOverlayOpen()
|
||||||
|
open()
|
||||||
|
modalKeyboardController.reset()
|
||||||
|
if (modalKeyboardController && notificationListRef) {
|
||||||
|
modalKeyboardController.listView = notificationListRef
|
||||||
|
modalKeyboardController.rebuildFlatNavigation()
|
||||||
|
|
||||||
|
Qt.callLater(() => {
|
||||||
|
modalKeyboardController.keyboardNavigationActive = true
|
||||||
|
modalKeyboardController.selectedFlatIndex = 0
|
||||||
|
modalKeyboardController.updateSelectedIdFromIndex()
|
||||||
|
if (notificationListRef) {
|
||||||
|
notificationListRef.keyboardActive = true
|
||||||
|
}
|
||||||
|
modalKeyboardController.selectionVersion++
|
||||||
|
modalKeyboardController.ensureVisible()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
notificationModalOpen = false
|
||||||
|
NotificationService.onOverlayClose()
|
||||||
|
close()
|
||||||
|
modalKeyboardController.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width: 500
|
||||||
|
height: 700
|
||||||
|
visible: false
|
||||||
|
onBackgroundClicked: hide()
|
||||||
|
onShouldBeVisibleChanged: (shouldBeVisible) => {
|
||||||
|
if (!shouldBeVisible) {
|
||||||
|
notificationModalOpen = false
|
||||||
|
modalKeyboardController.reset()
|
||||||
|
NotificationService.onOverlayClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modalFocusScope.Keys.onPressed: (event) => modalKeyboardController.handleKey(event)
|
||||||
|
|
||||||
|
NotificationKeyboardController {
|
||||||
|
id: modalKeyboardController
|
||||||
|
|
||||||
|
listView: null
|
||||||
|
isOpen: notificationModal.notificationModalOpen
|
||||||
|
onClose: () => notificationModal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
notificationModal.show();
|
||||||
|
return "NOTIFICATION_MODAL_OPEN_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
notificationModal.hide();
|
||||||
|
return "NOTIFICATION_MODAL_CLOSE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
notificationModal.toggle();
|
||||||
|
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "notifications"
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
id: notificationKeyHandler
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
NotificationHeader {
|
||||||
|
id: notificationHeader
|
||||||
|
|
||||||
|
keyboardController: modalKeyboardController
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationSettings {
|
||||||
|
id: notificationSettings
|
||||||
|
|
||||||
|
expanded: notificationHeader.showSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardNavigatedNotificationList {
|
||||||
|
id: notificationList
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - y
|
||||||
|
keyboardController: modalKeyboardController
|
||||||
|
Component.onCompleted: {
|
||||||
|
notificationModal.notificationListRef = notificationList
|
||||||
|
if (modalKeyboardController) {
|
||||||
|
modalKeyboardController.listView = notificationList
|
||||||
|
modalKeyboardController.rebuildFlatNavigation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationKeyboardHints {
|
||||||
|
id: keyboardHints
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
showHints: modalKeyboardController.showKeyboardHints
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
345
quickshell/.config/quickshell/Modals/PowerMenuModal.qml
Normal file
345
quickshell/.config/quickshell/Modals/PowerMenuModal.qml
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int selectedIndex: 0
|
||||||
|
property int optionCount: 4
|
||||||
|
|
||||||
|
signal powerActionRequested(string action, string title, string message)
|
||||||
|
|
||||||
|
function selectOption() {
|
||||||
|
close();
|
||||||
|
const actions = [{
|
||||||
|
"action": "logout",
|
||||||
|
"title": "Log Out",
|
||||||
|
"message": "Are you sure you want to log out?"
|
||||||
|
}, {
|
||||||
|
"action": "suspend",
|
||||||
|
"title": "Suspend",
|
||||||
|
"message": "Are you sure you want to suspend the system?"
|
||||||
|
}, {
|
||||||
|
"action": "reboot",
|
||||||
|
"title": "Reboot",
|
||||||
|
"message": "Are you sure you want to reboot the system?"
|
||||||
|
}, {
|
||||||
|
"action": "poweroff",
|
||||||
|
"title": "Power Off",
|
||||||
|
"message": "Are you sure you want to power off the system?"
|
||||||
|
}];
|
||||||
|
const selected = actions[selectedIndex];
|
||||||
|
if (selected) {
|
||||||
|
root.powerActionRequested(selected.action, selected.title, selected.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: false
|
||||||
|
width: 320
|
||||||
|
height: 300
|
||||||
|
enableShadow: true
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
return close();
|
||||||
|
}
|
||||||
|
onOpened: () => {
|
||||||
|
selectedIndex = 0;
|
||||||
|
modalFocusScope.forceActiveFocus();
|
||||||
|
}
|
||||||
|
modalFocusScope.Keys.onPressed: (event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Up:
|
||||||
|
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Down:
|
||||||
|
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
selectOption();
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Power Options"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - 150
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: () => {
|
||||||
|
return close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (selectedIndex === 0) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (logoutArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === 0 ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "logout"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Log Out"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: logoutArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = 0;
|
||||||
|
selectOption();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (selectedIndex === 1) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (suspendArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === 1 ? Theme.primary : "transparent"
|
||||||
|
border.width: selectedIndex === 1 ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "bedtime"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Suspend"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: suspendArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = 1;
|
||||||
|
selectOption();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (selectedIndex === 2) {
|
||||||
|
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||||
|
} else if (rebootArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === 2 ? Theme.warning : "transparent"
|
||||||
|
border.width: selectedIndex === 2 ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "restart_alt"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Reboot"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rebootArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = 2;
|
||||||
|
selectOption();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (selectedIndex === 3) {
|
||||||
|
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
|
||||||
|
} else if (powerOffArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
|
||||||
|
} else {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: selectedIndex === 3 ? Theme.error : "transparent"
|
||||||
|
border.width: selectedIndex === 3 ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "power_settings_new"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Power Off"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: powerOffArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
selectedIndex = 3;
|
||||||
|
selectOption();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
height: Theme.spacingS
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "↑↓ Navigate • Tab Cycle • Enter Select • Esc Close"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
356
quickshell/.config/quickshell/Modals/ProcessListModal.qml
Normal file
356
quickshell/.config/quickshell/Modals/ProcessListModal.qml
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Modules.ProcessList
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: processListModal
|
||||||
|
|
||||||
|
property int currentTab: 0
|
||||||
|
property var tabNames: ["Processes", "Performance", "System"]
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
if (!DgopService.dgopAvailable) {
|
||||||
|
console.warn("ProcessListModal: dgop is not available");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
open();
|
||||||
|
UserInfoService.getUptime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
close();
|
||||||
|
if (processContextMenu.visible) {
|
||||||
|
processContextMenu.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (!DgopService.dgopAvailable) {
|
||||||
|
console.warn("ProcessListModal: dgop is not available");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
hide();
|
||||||
|
} else {
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width: 900
|
||||||
|
height: 680
|
||||||
|
visible: false
|
||||||
|
backgroundColor: Theme.popupBackground()
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
enableShadow: true
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
return hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: processesTabComponent
|
||||||
|
|
||||||
|
ProcessesTab {
|
||||||
|
contextMenu: processContextMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: performanceTabComponent
|
||||||
|
|
||||||
|
PerformanceTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: systemTabComponent
|
||||||
|
|
||||||
|
SystemTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessContextMenu {
|
||||||
|
id: processContextMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
Keys.onPressed: (event) => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
processListModal.hide();
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_1) {
|
||||||
|
currentTab = 0;
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_2) {
|
||||||
|
currentTab = 1;
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_3) {
|
||||||
|
currentTab = 2;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message when dgop is not available
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 400
|
||||||
|
height: 200
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
|
||||||
|
border.color: Theme.error
|
||||||
|
border.width: 2
|
||||||
|
visible: !DgopService.dgopAvailable
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "error"
|
||||||
|
size: 48
|
||||||
|
color: Theme.error
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "System Monitor Unavailable"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.error
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature."
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
visible: DgopService.dgopAvailable
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "System Monitor"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge + 4
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: () => {
|
||||||
|
return processListModal.hide();
|
||||||
|
}
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 52
|
||||||
|
color: Theme.surfaceSelected
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 4
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: tabNames
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
|
||||||
|
height: 44
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||||
|
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||||
|
border.width: currentTab === index ? 1 : 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
const tabIcons = ["list_alt", "analytics", "settings"];
|
||||||
|
return tabIcons[index] || "tab";
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||||
|
opacity: currentTab === index ? 1 : 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.verticalCenterOffset: -1
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: tabMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
currentTab = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: processesTab
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
active: processListModal.visible && currentTab === 0
|
||||||
|
visible: currentTab === 0
|
||||||
|
opacity: currentTab === 0 ? 1 : 0
|
||||||
|
sourceComponent: processesTabComponent
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: performanceTab
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
active: processListModal.visible && currentTab === 1
|
||||||
|
visible: currentTab === 1
|
||||||
|
opacity: currentTab === 1 ? 1 : 0
|
||||||
|
sourceComponent: performanceTabComponent
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: systemTab
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
active: processListModal.visible && currentTab === 2
|
||||||
|
visible: currentTab === 2
|
||||||
|
opacity: currentTab === 2 ? 1 : 0
|
||||||
|
sourceComponent: systemTabComponent
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
211
quickshell/.config/quickshell/Modals/Settings/ProfileSection.qml
Normal file
211
quickshell/.config/quickshell/Modals/Settings/ProfileSection.qml
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var parentModal: null
|
||||||
|
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 110
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: profileImageContainer
|
||||||
|
|
||||||
|
property bool hasImage: profileImageSource.status === Image.Ready
|
||||||
|
|
||||||
|
width: 80
|
||||||
|
height: 80
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 1
|
||||||
|
visible: parent.hasImage
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: profileImageSource
|
||||||
|
|
||||||
|
source: {
|
||||||
|
if (PortalService.profileImage === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (PortalService.profileImage.startsWith("/")) {
|
||||||
|
return "file://" + PortalService.profileImage;
|
||||||
|
}
|
||||||
|
return PortalService.profileImage;
|
||||||
|
}
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
mipmap: true
|
||||||
|
cache: true
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiEffect {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 5
|
||||||
|
source: profileImageSource
|
||||||
|
maskEnabled: true
|
||||||
|
maskSource: profileCircularMask
|
||||||
|
visible: profileImageContainer.hasImage
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: profileCircularMask
|
||||||
|
|
||||||
|
width: 70
|
||||||
|
height: 70
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: "black"
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: Theme.primary
|
||||||
|
visible: !parent.hasImage
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "person"
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.primaryText
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.7)
|
||||||
|
visible: profileMouseArea.containsMouse
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: Qt.rgba(255, 255, 255, 0.9)
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "edit"
|
||||||
|
size: 16
|
||||||
|
color: "black"
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
if (root.parentModal) {
|
||||||
|
root.parentModal.allowFocusOverride = true;
|
||||||
|
root.parentModal.shouldHaveFocus = false;
|
||||||
|
if (root.parentModal.profileBrowser) {
|
||||||
|
root.parentModal.profileBrowser.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: Qt.rgba(255, 255, 255, 0.9)
|
||||||
|
visible: profileImageContainer.hasImage
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "close"
|
||||||
|
size: 16
|
||||||
|
color: "black"
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
return PortalService.setProfileImage("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: profileMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
propagateComposedEvents: true
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: 120
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: UserInfoService.fullName || "User"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: DgopService.distribution || "Linux"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.Settings
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int currentIndex: 0
|
||||||
|
property var parentModal: null
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 0
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: 0
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: personalizationLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 0
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: Component {
|
||||||
|
PersonalizationTab {
|
||||||
|
parentModal: root.parentModal
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: timeLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 1
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: TimeTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: weatherLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 2
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: WeatherTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: topBarLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 3
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: TopBarTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: widgetsLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 4
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: WidgetTweaksTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: displaysLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 6
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: DisplaysTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: recentAppsLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 7
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: RecentAppsTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: themeColorsLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 8
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: ThemeColorsTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: aboutLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 9
|
||||||
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: AboutTab {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
198
quickshell/.config/quickshell/Modals/Settings/SettingsModal.qml
Normal file
198
quickshell/.config/quickshell/Modals/Settings/SettingsModal.qml
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Modals.FileBrowser
|
||||||
|
import qs.Modules.Settings
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: settingsModal
|
||||||
|
|
||||||
|
property Component settingsContent
|
||||||
|
|
||||||
|
signal closingModal()
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
hide();
|
||||||
|
} else {
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objectName: "settingsModal"
|
||||||
|
width: 800
|
||||||
|
height: 750
|
||||||
|
visible: false
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
return hide();
|
||||||
|
}
|
||||||
|
content: settingsContent
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
settingsModal.show();
|
||||||
|
return "SETTINGS_OPEN_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
settingsModal.hide();
|
||||||
|
return "SETTINGS_CLOSE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
settingsModal.toggle();
|
||||||
|
return "SETTINGS_TOGGLE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "settings"
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function browse(type: string) {
|
||||||
|
if (type === "wallpaper") {
|
||||||
|
wallpaperBrowser.allowStacking = false;
|
||||||
|
wallpaperBrowser.open();
|
||||||
|
} else if (type === "profile") {
|
||||||
|
profileBrowser.allowStacking = false;
|
||||||
|
profileBrowser.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
FileBrowserModal {
|
||||||
|
id: profileBrowser
|
||||||
|
|
||||||
|
allowStacking: true
|
||||||
|
browserTitle: "Select Profile Image"
|
||||||
|
browserIcon: "person"
|
||||||
|
browserType: "profile"
|
||||||
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||||
|
onFileSelected: (path) => {
|
||||||
|
PortalService.setProfileImage(path);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
onDialogClosed: () => {
|
||||||
|
if (settingsModal) {
|
||||||
|
settingsModal.allowFocusOverride = false;
|
||||||
|
settingsModal.shouldHaveFocus = Qt.binding(() => {
|
||||||
|
return settingsModal.shouldBeVisible;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
allowStacking = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileBrowserModal {
|
||||||
|
id: wallpaperBrowser
|
||||||
|
|
||||||
|
allowStacking: true
|
||||||
|
browserTitle: "Select Wallpaper"
|
||||||
|
browserIcon: "wallpaper"
|
||||||
|
browserType: "wallpaper"
|
||||||
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||||
|
onFileSelected: (path) => {
|
||||||
|
SessionData.setWallpaper(path);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
onDialogClosed: () => {
|
||||||
|
allowStacking = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsContent: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.rightMargin: Theme.spacingL
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
anchors.bottomMargin: Theme.spacingL
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 35
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "settings"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
circular: false
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: () => {
|
||||||
|
return settingsModal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 35
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
SettingsSidebar {
|
||||||
|
id: sidebar
|
||||||
|
|
||||||
|
parentModal: settingsModal
|
||||||
|
onCurrentIndexChanged: content.currentIndex = currentIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsContent {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
width: parent.width - sidebar.width
|
||||||
|
height: parent.height
|
||||||
|
parentModal: settingsModal
|
||||||
|
currentIndex: sidebar.currentIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Settings
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: sidebarContainer
|
||||||
|
|
||||||
|
property int currentIndex: 0
|
||||||
|
property var parentModal: null
|
||||||
|
readonly property var sidebarItems: [{
|
||||||
|
"text": "Personalization",
|
||||||
|
"icon": "person"
|
||||||
|
}, {
|
||||||
|
"text": "Time & Date",
|
||||||
|
"icon": "schedule"
|
||||||
|
}, {
|
||||||
|
"text": "Weather",
|
||||||
|
"icon": "cloud"
|
||||||
|
}, {
|
||||||
|
"text": "Top Bar",
|
||||||
|
"icon": "toolbar"
|
||||||
|
}, {
|
||||||
|
"text": "Widgets",
|
||||||
|
"icon": "widgets"
|
||||||
|
}, {
|
||||||
|
"text": "Displays",
|
||||||
|
"icon": "monitor"
|
||||||
|
}, {
|
||||||
|
"text": "Recent Apps",
|
||||||
|
"icon": "history"
|
||||||
|
}, {
|
||||||
|
"text": "Theme & Colors",
|
||||||
|
"icon": "palette"
|
||||||
|
}, {
|
||||||
|
"text": "About",
|
||||||
|
"icon": "info"
|
||||||
|
}]
|
||||||
|
|
||||||
|
width: 270
|
||||||
|
height: parent.height
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
anchors.topMargin: Theme.spacingM + 2
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
ProfileSection {
|
||||||
|
parentModal: sidebarContainer.parentModal
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: Theme.spacingL
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: sidebarRepeater
|
||||||
|
|
||||||
|
model: sidebarContainer.sidebarItems
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isActive: sidebarContainer.currentIndex === index
|
||||||
|
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 44
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: isActive ? Theme.surfaceContainerHigh : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: modelData.icon || ""
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: parent.parent.isActive ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.text || ""
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: parent.parent.isActive ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: tabMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
sidebarContainer.currentIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Spotlight
|
||||||
|
import qs.Modules.AppDrawer
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: spotlightKeyHandler
|
||||||
|
|
||||||
|
property alias appLauncher: appLauncher
|
||||||
|
property alias searchField: searchField
|
||||||
|
property var parentModal: null
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (parentModal)
|
||||||
|
parentModal.hide()
|
||||||
|
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Down) {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Up) {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectNextInRow()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectPreviousInRow()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
appLauncher.launchSelected()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) {
|
||||||
|
searchField.forceActiveFocus()
|
||||||
|
searchField.insertText(event.text)
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLauncher {
|
||||||
|
id: appLauncher
|
||||||
|
|
||||||
|
viewMode: SettingsData.spotlightModalViewMode
|
||||||
|
gridColumns: 4
|
||||||
|
onAppLaunched: () => {
|
||||||
|
if (parentModal)
|
||||||
|
parentModal.hide()
|
||||||
|
}
|
||||||
|
onViewModeSelected: mode => {
|
||||||
|
SettingsData.setSpotlightModalViewMode(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: categorySelector.height + Theme.spacingM * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceVariantAlpha
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: 1
|
||||||
|
visible: appLauncher.categories.length > 1 || appLauncher.model.count > 0
|
||||||
|
|
||||||
|
CategorySelector {
|
||||||
|
id: categorySelector
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
categories: appLauncher.categories
|
||||||
|
selectedCategory: appLauncher.selectedCategory
|
||||||
|
compact: false
|
||||||
|
onCategorySelected: category => {
|
||||||
|
appLauncher.setCategory(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
|
||||||
|
width: parent.width - 80 - Theme.spacingM
|
||||||
|
height: 56
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
|
||||||
|
normalBorderColor: Theme.outlineMedium
|
||||||
|
focusedBorderColor: Theme.primary
|
||||||
|
leftIconName: "search"
|
||||||
|
leftIconSize: Theme.iconSize
|
||||||
|
leftIconColor: Theme.surfaceVariantText
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
|
showClearButton: true
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
enabled: parentModal ? parentModal.spotlightOpen : true
|
||||||
|
placeholderText: ""
|
||||||
|
ignoreLeftRightKeys: true
|
||||||
|
keyForwardTargets: [spotlightKeyHandler]
|
||||||
|
text: appLauncher.searchQuery
|
||||||
|
onTextEdited: () => {
|
||||||
|
appLauncher.searchQuery = text
|
||||||
|
}
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (parentModal)
|
||||||
|
parentModal.hide()
|
||||||
|
|
||||||
|
event.accepted = true
|
||||||
|
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
|
||||||
|
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
|
||||||
|
appLauncher.launchSelected()
|
||||||
|
else if (appLauncher.model.count > 0)
|
||||||
|
appLauncher.launchApp(appLauncher.model.get(0))
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||||
|
event.accepted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: appLauncher.model.count > 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
border.color: appLauncher.viewMode === "list" ? Theme.primarySelected : "transparent"
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "view_list"
|
||||||
|
size: 18
|
||||||
|
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: listViewArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
appLauncher.setViewMode("list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
border.color: appLauncher.viewMode === "grid" ? Theme.primarySelected : "transparent"
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "grid_view"
|
||||||
|
size: 18
|
||||||
|
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: gridViewArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
appLauncher.setViewMode("grid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpotlightResults {
|
||||||
|
appLauncher: spotlightKeyHandler.appLauncher
|
||||||
|
contextMenu: contextMenu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpotlightContextMenu {
|
||||||
|
id: contextMenu
|
||||||
|
|
||||||
|
appLauncher: spotlightKeyHandler.appLauncher
|
||||||
|
parentHandler: spotlightKeyHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: contextMenu.visible
|
||||||
|
z: 999
|
||||||
|
onClicked: () => {
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
|
||||||
|
// Prevent closing when clicking on the menu itself
|
||||||
|
x: contextMenu.x
|
||||||
|
y: contextMenu.y
|
||||||
|
width: contextMenu.width
|
||||||
|
height: contextMenu.height
|
||||||
|
onClicked: () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: contextMenu
|
||||||
|
|
||||||
|
property var currentApp: null
|
||||||
|
property bool menuVisible: false
|
||||||
|
property var appLauncher: null
|
||||||
|
property var parentHandler: null
|
||||||
|
|
||||||
|
function show(x, y, app) {
|
||||||
|
currentApp = app
|
||||||
|
const menuWidth = 180
|
||||||
|
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
|
let finalX = x + 8
|
||||||
|
let finalY = y + 8
|
||||||
|
if (parentHandler) {
|
||||||
|
if (finalX + menuWidth > parentHandler.width)
|
||||||
|
finalX = x - menuWidth - 8
|
||||||
|
|
||||||
|
if (finalY + menuHeight > parentHandler.height)
|
||||||
|
finalY = y - menuHeight - 8
|
||||||
|
|
||||||
|
finalX = Math.max(8, Math.min(finalX, parentHandler.width - menuWidth - 8))
|
||||||
|
finalY = Math.max(8, Math.min(finalY, parentHandler.height - menuHeight - 8))
|
||||||
|
}
|
||||||
|
contextMenu.x = finalX
|
||||||
|
contextMenu.y = finalY
|
||||||
|
contextMenu.visible = true
|
||||||
|
contextMenu.menuVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
contextMenu.menuVisible = false
|
||||||
|
Qt.callLater(() => {
|
||||||
|
contextMenu.visible = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
width: 180
|
||||||
|
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
z: 1000
|
||||||
|
opacity: menuVisible ? 1 : 0
|
||||||
|
scale: menuVisible ? 1 : 0.85
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: 4
|
||||||
|
anchors.leftMargin: 2
|
||||||
|
anchors.rightMargin: -2
|
||||||
|
anchors.bottomMargin: -4
|
||||||
|
radius: parent.radius
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.15)
|
||||||
|
z: parent.z - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: menuColumn
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||||
|
return "push_pin"
|
||||||
|
|
||||||
|
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||||
|
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||||
|
return "Pin to Dock"
|
||||||
|
|
||||||
|
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||||
|
return SessionData.isPinnedApp(appId) ? "Unpin from Dock" : "Pin to Dock"
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: pinMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
|
||||||
|
return
|
||||||
|
|
||||||
|
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
|
||||||
|
if (SessionData.isPinnedApp(appId))
|
||||||
|
SessionData.removePinnedApp(appId)
|
||||||
|
else
|
||||||
|
SessionData.addPinnedApp(appId)
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 5
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "launch"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Launch"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: launchMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
if (contextMenu.currentApp && appLauncher)
|
||||||
|
appLauncher.launchApp(contextMenu.currentApp)
|
||||||
|
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Modules.AppDrawer
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: spotlightModal
|
||||||
|
|
||||||
|
property bool spotlightOpen: false
|
||||||
|
property Component spotlightContent
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
spotlightOpen = true
|
||||||
|
open()
|
||||||
|
if (contentLoader.item && contentLoader.item.appLauncher) {
|
||||||
|
contentLoader.item.appLauncher.searchQuery = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item && contentLoader.item.searchField) {
|
||||||
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
spotlightOpen = false
|
||||||
|
close()
|
||||||
|
if (contentLoader.item && contentLoader.item.appLauncher) {
|
||||||
|
contentLoader.item.appLauncher.searchQuery = ""
|
||||||
|
contentLoader.item.appLauncher.selectedIndex = 0
|
||||||
|
contentLoader.item.appLauncher.setCategory("All")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (spotlightOpen) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: spotlightOpen
|
||||||
|
width: 550
|
||||||
|
height: 600
|
||||||
|
backgroundColor: Theme.popupBackground()
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
borderColor: Theme.outlineMedium
|
||||||
|
borderWidth: 1
|
||||||
|
enableShadow: true
|
||||||
|
onVisibleChanged: () => {
|
||||||
|
if (visible && !spotlightOpen) {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
if (visible && contentLoader.item) {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item.searchField) {
|
||||||
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
return hide()
|
||||||
|
}
|
||||||
|
content: spotlightContent
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCloseAllModalsExcept(excludedModal) {
|
||||||
|
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
|
||||||
|
spotlightOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: ModalManager
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
spotlightModal.show()
|
||||||
|
return "SPOTLIGHT_OPEN_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
spotlightModal.hide()
|
||||||
|
return "SPOTLIGHT_CLOSE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
spotlightModal.toggle()
|
||||||
|
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "spotlight"
|
||||||
|
}
|
||||||
|
|
||||||
|
spotlightContent: Component {
|
||||||
|
SpotlightContent {
|
||||||
|
parentModal: spotlightModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,324 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: resultsContainer
|
||||||
|
|
||||||
|
property var appLauncher: null
|
||||||
|
property var contextMenu: null
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - y
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
id: resultsList
|
||||||
|
|
||||||
|
property int itemHeight: 60
|
||||||
|
property int iconSize: 40
|
||||||
|
property bool showDescription: true
|
||||||
|
property int itemSpacing: Theme.spacingS
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||||
|
|
||||||
|
signal keyboardNavigationReset
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return
|
||||||
|
|
||||||
|
const itemY = index * (itemHeight + itemSpacing)
|
||||||
|
const itemBottom = itemY + itemHeight
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
visible: appLauncher && appLauncher.viewMode === "list"
|
||||||
|
model: appLauncher ? appLauncher.model : null
|
||||||
|
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||||
|
clip: true
|
||||||
|
spacing: itemSpacing
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
onItemClicked: (index, modelData) => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.launchApp(modelData)
|
||||||
|
}
|
||||||
|
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||||
|
if (contextMenu)
|
||||||
|
contextMenu.show(mouseX, mouseY, modelData)
|
||||||
|
}
|
||||||
|
onKeyboardNavigationReset: () => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: resultsList.itemHeight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||||
|
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
|
||||||
|
border.width: ListView.isCurrentItem ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: resultsList.iconSize
|
||||||
|
height: resultsList.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: listIconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: Quickshell.iconPath(model.icon, true) || DesktopService.resolveIconPath(model.icon) || DesktopService.resolveIconPath(model.startupClass)
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !listIconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: resultsList.iconSize * 0.4
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - resultsList.iconSize - Theme.spacingL
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.comment || "Application"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: resultsList.showDescription && model.comment && model.comment.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: listMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: () => {
|
||||||
|
if (resultsList.hoverUpdatesSelection && !resultsList.keyboardNavigationActive)
|
||||||
|
resultsList.currentIndex = index
|
||||||
|
}
|
||||||
|
onPositionChanged: () => {
|
||||||
|
resultsList.keyboardNavigationReset()
|
||||||
|
}
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
resultsList.itemClicked(index, model)
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y)
|
||||||
|
resultsList.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankGridView {
|
||||||
|
id: resultsGrid
|
||||||
|
|
||||||
|
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||||
|
property int columns: 4
|
||||||
|
property bool adaptiveColumns: false
|
||||||
|
property int minCellWidth: 120
|
||||||
|
property int maxCellWidth: 160
|
||||||
|
property int cellPadding: 8
|
||||||
|
property real iconSizeRatio: 0.55
|
||||||
|
property int maxIconSize: 48
|
||||||
|
property int minIconSize: 32
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||||
|
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||||
|
property int baseCellHeight: baseCellWidth + 20
|
||||||
|
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||||
|
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||||
|
|
||||||
|
signal keyboardNavigationReset
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return
|
||||||
|
|
||||||
|
const itemY = Math.floor(index / actualColumns) * cellHeight
|
||||||
|
const itemBottom = itemY + cellHeight
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
visible: appLauncher && appLauncher.viewMode === "grid"
|
||||||
|
model: appLauncher ? appLauncher.model : null
|
||||||
|
clip: true
|
||||||
|
cellWidth: baseCellWidth
|
||||||
|
cellHeight: baseCellHeight
|
||||||
|
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||||
|
rightMargin: leftMargin
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
onItemClicked: (index, modelData) => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.launchApp(modelData)
|
||||||
|
}
|
||||||
|
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||||
|
if (contextMenu)
|
||||||
|
contextMenu.show(mouseX, mouseY, modelData)
|
||||||
|
}
|
||||||
|
onKeyboardNavigationReset: () => {
|
||||||
|
if (appLauncher)
|
||||||
|
appLauncher.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: resultsGrid.cellWidth - resultsGrid.cellPadding
|
||||||
|
height: resultsGrid.cellHeight - resultsGrid.cellPadding
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||||
|
border.color: resultsGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
|
||||||
|
border.width: resultsGrid.currentIndex === index ? 2 : 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int iconSize: Math.min(resultsGrid.maxIconSize, Math.max(resultsGrid.minIconSize, resultsGrid.cellWidth * resultsGrid.iconSizeRatio))
|
||||||
|
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: gridIconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: Quickshell.iconPath(model.icon, true) || DesktopService.resolveIconPath(model.icon) || DesktopService.resolveIconPath(model.startupClass)
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !gridIconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: Math.min(28, parent.width * 0.5)
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: resultsGrid.cellWidth - 12
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: gridMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: () => {
|
||||||
|
if (resultsGrid.hoverUpdatesSelection && !resultsGrid.keyboardNavigationActive)
|
||||||
|
resultsGrid.currentIndex = index
|
||||||
|
}
|
||||||
|
onPositionChanged: () => {
|
||||||
|
resultsGrid.keyboardNavigationReset()
|
||||||
|
}
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
resultsGrid.itemClicked(index, model)
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y)
|
||||||
|
resultsGrid.itemRightClicked(index, model, modalPos.x, modalPos.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
296
quickshell/.config/quickshell/Modals/WifiPasswordModal.qml
Normal file
296
quickshell/.config/quickshell/Modals/WifiPasswordModal.qml
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string wifiPasswordSSID: ""
|
||||||
|
property string wifiPasswordInput: ""
|
||||||
|
|
||||||
|
function show(ssid) {
|
||||||
|
wifiPasswordSSID = ssid
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
open()
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item && contentLoader.item.passwordInput)
|
||||||
|
contentLoader.item.passwordInput.forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: false
|
||||||
|
width: 420
|
||||||
|
height: 230
|
||||||
|
onShouldBeVisibleChanged: () => {
|
||||||
|
if (!shouldBeVisible)
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
}
|
||||||
|
onOpened: {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item && contentLoader.item.passwordInput)
|
||||||
|
contentLoader.item.passwordInput.forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onBackgroundClicked: () => {
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: NetworkService
|
||||||
|
|
||||||
|
function onPasswordDialogShouldReopenChanged() {
|
||||||
|
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
|
||||||
|
wifiPasswordSSID = NetworkService.connectingSSID
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
open()
|
||||||
|
NetworkService.passwordDialogShouldReopen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
FocusScope {
|
||||||
|
id: wifiContent
|
||||||
|
|
||||||
|
property alias passwordInput: passwordInput
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
Keys.onEscapePressed: event => {
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - 40
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Connect to Wi-Fi"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: `Enter password for "${wifiPasswordSSID}"`
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: () => {
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceHover
|
||||||
|
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||||
|
border.width: passwordInput.activeFocus ? 2 : 1
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: () => {
|
||||||
|
passwordInput.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: passwordInput
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
text: wifiPasswordInput
|
||||||
|
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
||||||
|
placeholderText: ""
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
focus: true
|
||||||
|
enabled: root.shouldBeVisible
|
||||||
|
onTextEdited: () => {
|
||||||
|
wifiPasswordInput = text
|
||||||
|
}
|
||||||
|
onAccepted: () => {
|
||||||
|
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
passwordInput.text = ""
|
||||||
|
}
|
||||||
|
Component.onCompleted: () => {
|
||||||
|
if (root.shouldBeVisible)
|
||||||
|
focusDelayTimer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: focusDelayTimer
|
||||||
|
|
||||||
|
interval: 100
|
||||||
|
repeat: false
|
||||||
|
onTriggered: () => {
|
||||||
|
if (root.shouldBeVisible)
|
||||||
|
passwordInput.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
|
||||||
|
function onShouldBeVisibleChanged() {
|
||||||
|
if (root.shouldBeVisible)
|
||||||
|
focusDelayTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: showPasswordCheckbox
|
||||||
|
|
||||||
|
property bool checked: false
|
||||||
|
|
||||||
|
width: 20
|
||||||
|
height: 20
|
||||||
|
radius: 4
|
||||||
|
color: checked ? Theme.primary : "transparent"
|
||||||
|
border.color: checked ? Theme.primary : Theme.outlineButton
|
||||||
|
border.width: 2
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "check"
|
||||||
|
size: 12
|
||||||
|
color: Theme.background
|
||||||
|
visible: parent.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Show password"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||||
|
border.color: Theme.surfaceVariantAlpha
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: cancelText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Cancel"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cancelArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||||
|
enabled: passwordInput.text.length > 0
|
||||||
|
opacity: enabled ? 1 : 0.5
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: connectText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Connect"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.background
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: connectArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: parent.enabled
|
||||||
|
onClicked: () => {
|
||||||
|
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
|
||||||
|
close()
|
||||||
|
wifiPasswordInput = ""
|
||||||
|
passwordInput.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,821 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.AppDrawer
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankPopout {
|
||||||
|
id: appDrawerPopout
|
||||||
|
|
||||||
|
property string triggerSection: "left"
|
||||||
|
property var triggerScreen: null
|
||||||
|
|
||||||
|
// Setting to Exclusive, so virtual keyboards can send input to app drawer
|
||||||
|
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTriggerPosition(x, y, width, section, screen) {
|
||||||
|
triggerX = x
|
||||||
|
triggerY = y
|
||||||
|
triggerWidth = width
|
||||||
|
triggerSection = section
|
||||||
|
triggerScreen = screen
|
||||||
|
}
|
||||||
|
|
||||||
|
popupWidth: 520
|
||||||
|
popupHeight: 600
|
||||||
|
triggerX: Theme.spacingL
|
||||||
|
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
||||||
|
triggerWidth: 40
|
||||||
|
positioning: "center"
|
||||||
|
screen: triggerScreen
|
||||||
|
|
||||||
|
onShouldBeVisibleChanged: {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
appLauncher.searchQuery = ""
|
||||||
|
appLauncher.selectedIndex = 0
|
||||||
|
appLauncher.setCategory("All")
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item && contentLoader.item.searchField) {
|
||||||
|
contentLoader.item.searchField.text = ""
|
||||||
|
contentLoader.item.searchField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLauncher {
|
||||||
|
id: appLauncher
|
||||||
|
|
||||||
|
viewMode: SettingsData.appLauncherViewMode
|
||||||
|
gridColumns: 4
|
||||||
|
onAppLaunched: appDrawerPopout.close()
|
||||||
|
onViewModeSelected: function (mode) {
|
||||||
|
SettingsData.setAppLauncherViewMode(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Rectangle {
|
||||||
|
id: launcherPanel
|
||||||
|
|
||||||
|
property alias searchField: searchField
|
||||||
|
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
antialiasing: true
|
||||||
|
smooth: true
|
||||||
|
|
||||||
|
// Multi-layer border effect
|
||||||
|
Repeater {
|
||||||
|
model: [{
|
||||||
|
"margin": -3,
|
||||||
|
"color": Qt.rgba(0, 0, 0, 0.05),
|
||||||
|
"z": -3
|
||||||
|
}, {
|
||||||
|
"margin": -2,
|
||||||
|
"color": Qt.rgba(0, 0, 0, 0.08),
|
||||||
|
"z": -2
|
||||||
|
}, {
|
||||||
|
"margin": 0,
|
||||||
|
"color": Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12),
|
||||||
|
"z": -1
|
||||||
|
}]
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: modelData.margin
|
||||||
|
color: "transparent"
|
||||||
|
radius: parent.radius + Math.abs(modelData.margin)
|
||||||
|
border.color: modelData.color
|
||||||
|
border.width: 1
|
||||||
|
z: modelData.z
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: keyHandler
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
readonly property var keyMappings: {
|
||||||
|
const mappings = {}
|
||||||
|
mappings[Qt.Key_Escape] = () => appDrawerPopout.close()
|
||||||
|
mappings[Qt.Key_Down] = () => appLauncher.selectNext()
|
||||||
|
mappings[Qt.Key_Up] = () => appLauncher.selectPrevious()
|
||||||
|
mappings[Qt.Key_Return] = () => appLauncher.launchSelected()
|
||||||
|
mappings[Qt.Key_Enter] = () => appLauncher.launchSelected()
|
||||||
|
|
||||||
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
mappings[Qt.Key_Right] = () => appLauncher.selectNextInRow()
|
||||||
|
mappings[Qt.Key_Left] = () => appLauncher.selectPreviousInRow()
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappings
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
if (keyMappings[event.key]) {
|
||||||
|
keyMappings[event.key]()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchField.activeFocus && event.text && /[a-zA-Z0-9\s]/.test(event.text)) {
|
||||||
|
searchField.forceActiveFocus()
|
||||||
|
searchField.insertText(event.text)
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - Theme.spacingL * 2
|
||||||
|
height: parent.height - Theme.spacingL * 2
|
||||||
|
x: Theme.spacingL
|
||||||
|
y: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Applications"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge + 4
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - 200
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: appLauncher.model.count + " apps"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 52
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
|
||||||
|
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
|
focusedBorderColor: Theme.primary
|
||||||
|
leftIconName: "search"
|
||||||
|
leftIconSize: Theme.iconSize
|
||||||
|
leftIconColor: Theme.surfaceVariantText
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
|
showClearButton: true
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
enabled: appDrawerPopout.shouldBeVisible
|
||||||
|
ignoreLeftRightKeys: true
|
||||||
|
keyForwardTargets: [keyHandler]
|
||||||
|
onTextEdited: {
|
||||||
|
appLauncher.searchQuery = text
|
||||||
|
}
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
appDrawerPopout.close()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key)
|
||||||
|
const hasText = text.length > 0
|
||||||
|
|
||||||
|
if (isEnterKey && hasText) {
|
||||||
|
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
|
||||||
|
appLauncher.launchSelected()
|
||||||
|
} else if (appLauncher.model.count > 0) {
|
||||||
|
appLauncher.launchApp(appLauncher.model.get(0))
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right]
|
||||||
|
const isNavigationKey = navigationKeys.includes(event.key)
|
||||||
|
const isEmptyEnter = isEnterKey && !hasText
|
||||||
|
|
||||||
|
event.accepted = !(isNavigationKey || isEmptyEnter)
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onShouldBeVisibleChanged() {
|
||||||
|
if (!appDrawerPopout.shouldBeVisible) {
|
||||||
|
searchField.focus = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: appDrawerPopout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: searchField.text.length === 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 200
|
||||||
|
height: 36
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
anchors.fill: parent
|
||||||
|
text: ""
|
||||||
|
currentValue: appLauncher.selectedCategory
|
||||||
|
options: appLauncher.categories
|
||||||
|
optionIcons: appLauncher.categoryIcons
|
||||||
|
onValueChanged: function (value) {
|
||||||
|
appLauncher.setCategory(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - 300
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 36
|
||||||
|
circular: false
|
||||||
|
iconName: "view_list"
|
||||||
|
iconSize: 20
|
||||||
|
iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||||
|
backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
onClicked: {
|
||||||
|
appLauncher.setViewMode("list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 36
|
||||||
|
circular: false
|
||||||
|
iconName: "grid_view"
|
||||||
|
iconSize: 20
|
||||||
|
iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||||
|
backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
onClicked: {
|
||||||
|
appLauncher.setViewMode("grid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: {
|
||||||
|
let usedHeight = 40 + Theme.spacingL
|
||||||
|
usedHeight += 52 + Theme.spacingL
|
||||||
|
usedHeight += (searchField.text.length === 0 ? 40 + Theme.spacingL : 0)
|
||||||
|
return parent.height - usedHeight
|
||||||
|
}
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
id: appList
|
||||||
|
|
||||||
|
property int itemHeight: 72
|
||||||
|
property int iconSize: 56
|
||||||
|
property bool showDescription: true
|
||||||
|
property int itemSpacing: Theme.spacingS
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
||||||
|
|
||||||
|
signal keyboardNavigationReset
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return
|
||||||
|
|
||||||
|
var itemY = index * (itemHeight + itemSpacing)
|
||||||
|
var itemBottom = itemY + itemHeight
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
visible: appLauncher.viewMode === "list"
|
||||||
|
model: appLauncher.model
|
||||||
|
currentIndex: appLauncher.selectedIndex
|
||||||
|
clip: true
|
||||||
|
spacing: itemSpacing
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
onItemClicked: function (index, modelData) {
|
||||||
|
appLauncher.launchApp(modelData)
|
||||||
|
}
|
||||||
|
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
||||||
|
contextMenu.show(mouseX, mouseY, modelData)
|
||||||
|
}
|
||||||
|
onKeyboardNavigationReset: {
|
||||||
|
appLauncher.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: appList.itemHeight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||||
|
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
|
||||||
|
border.width: ListView.isCurrentItem ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: appList.iconSize
|
||||||
|
height: appList.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: listIconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: Quickshell.iconPath(model.icon, true) || DesktopService.resolveIconPath(model.icon) || DesktopService.resolveIconPath(model.startupClass)
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !listIconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: appList.iconSize * 0.4
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - appList.iconSize - Theme.spacingL
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.comment || "Application"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: appList.showDescription && model.comment && model.comment.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: listMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: {
|
||||||
|
if (appList.hoverUpdatesSelection && !appList.keyboardNavigationActive)
|
||||||
|
appList.currentIndex = index
|
||||||
|
}
|
||||||
|
onPositionChanged: {
|
||||||
|
appList.keyboardNavigationReset()
|
||||||
|
}
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
appList.itemClicked(index, model)
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y)
|
||||||
|
appList.itemRightClicked(index, model, panelPos.x, panelPos.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankGridView {
|
||||||
|
id: appGrid
|
||||||
|
|
||||||
|
property int currentIndex: appLauncher.selectedIndex
|
||||||
|
property int columns: 4
|
||||||
|
property bool adaptiveColumns: false
|
||||||
|
property int minCellWidth: 120
|
||||||
|
property int maxCellWidth: 160
|
||||||
|
property int cellPadding: 8
|
||||||
|
property real iconSizeRatio: 0.6
|
||||||
|
property int maxIconSize: 56
|
||||||
|
property int minIconSize: 32
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
||||||
|
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||||
|
property int baseCellHeight: baseCellWidth + 20
|
||||||
|
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||||
|
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||||
|
|
||||||
|
signal keyboardNavigationReset
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return
|
||||||
|
|
||||||
|
var itemY = Math.floor(index / actualColumns) * cellHeight
|
||||||
|
var itemBottom = itemY + cellHeight
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
visible: appLauncher.viewMode === "grid"
|
||||||
|
model: appLauncher.model
|
||||||
|
clip: true
|
||||||
|
cellWidth: baseCellWidth
|
||||||
|
cellHeight: baseCellHeight
|
||||||
|
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||||
|
rightMargin: leftMargin
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
onItemClicked: function (index, modelData) {
|
||||||
|
appLauncher.launchApp(modelData)
|
||||||
|
}
|
||||||
|
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
|
||||||
|
contextMenu.show(mouseX, mouseY, modelData)
|
||||||
|
}
|
||||||
|
onKeyboardNavigationReset: {
|
||||||
|
appLauncher.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: appGrid.cellWidth - appGrid.cellPadding
|
||||||
|
height: appGrid.cellHeight - appGrid.cellPadding
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: appGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||||
|
border.color: appGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
|
||||||
|
border.width: appGrid.currentIndex === index ? 2 : 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int iconSize: Math.min(appGrid.maxIconSize, Math.max(appGrid.minIconSize, appGrid.cellWidth * appGrid.iconSizeRatio))
|
||||||
|
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: gridIconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: Quickshell.iconPath(model.icon, true) || DesktopService.resolveIconPath(model.icon)
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !gridIconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: Math.min(28, parent.width * 0.5)
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: appGrid.cellWidth - 12
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: gridMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: {
|
||||||
|
if (appGrid.hoverUpdatesSelection && !appGrid.keyboardNavigationActive)
|
||||||
|
appGrid.currentIndex = index
|
||||||
|
}
|
||||||
|
onPositionChanged: {
|
||||||
|
appGrid.keyboardNavigationReset()
|
||||||
|
}
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
appGrid.itemClicked(index, model)
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y)
|
||||||
|
appGrid.itemRightClicked(index, model, panelPos.x, panelPos.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: contextMenu
|
||||||
|
|
||||||
|
property var currentApp: null
|
||||||
|
property bool menuVisible: false
|
||||||
|
|
||||||
|
readonly property string appId: (currentApp && currentApp.desktopEntry) ? (currentApp.desktopEntry.id || currentApp.desktopEntry.execString || "") : ""
|
||||||
|
readonly property bool isPinned: appId && SessionData.isPinnedApp(appId)
|
||||||
|
|
||||||
|
function show(x, y, app) {
|
||||||
|
currentApp = app
|
||||||
|
|
||||||
|
const menuWidth = 180
|
||||||
|
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
|
|
||||||
|
let finalX = x + 8
|
||||||
|
let finalY = y + 8
|
||||||
|
|
||||||
|
if (finalX + menuWidth > appDrawerPopout.popupWidth) {
|
||||||
|
finalX = x - menuWidth - 8
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalY + menuHeight > appDrawerPopout.popupHeight) {
|
||||||
|
finalY = y - menuHeight - 8
|
||||||
|
}
|
||||||
|
|
||||||
|
finalX = Math.max(8, Math.min(finalX, appDrawerPopout.popupWidth - menuWidth - 8))
|
||||||
|
finalY = Math.max(8, Math.min(finalY, appDrawerPopout.popupHeight - menuHeight - 8))
|
||||||
|
|
||||||
|
contextMenu.x = finalX
|
||||||
|
contextMenu.y = finalY
|
||||||
|
contextMenu.visible = true
|
||||||
|
contextMenu.menuVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
contextMenu.menuVisible = false
|
||||||
|
Qt.callLater(() => {
|
||||||
|
contextMenu.visible = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
width: 180
|
||||||
|
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
z: 1000
|
||||||
|
opacity: menuVisible ? 1 : 0
|
||||||
|
scale: menuVisible ? 1 : 0.85
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: 4
|
||||||
|
anchors.leftMargin: 2
|
||||||
|
anchors.rightMargin: -2
|
||||||
|
anchors.bottomMargin: -4
|
||||||
|
radius: parent.radius
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.15)
|
||||||
|
z: parent.z - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: menuColumn
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: contextMenu.isPinned ? "keep_off" : "push_pin"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: contextMenu.isPinned ? "Unpin from Dock" : "Pin to Dock"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: pinMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextMenu.isPinned) {
|
||||||
|
SessionData.removePinnedApp(contextMenu.appId)
|
||||||
|
} else {
|
||||||
|
SessionData.addPinnedApp(contextMenu.appId)
|
||||||
|
}
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 5
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "launch"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Launch"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: launchMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (contextMenu.currentApp)
|
||||||
|
appLauncher.launchApp(contextMenu.currentApp)
|
||||||
|
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: contextMenu.visible
|
||||||
|
z: 999
|
||||||
|
onClicked: {
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
x: contextMenu.x
|
||||||
|
y: contextMenu.y
|
||||||
|
width: contextMenu.width
|
||||||
|
height: contextMenu.height
|
||||||
|
onClicked: {
|
||||||
|
|
||||||
|
// Prevent closing when clicking on the menu itself
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
168
quickshell/.config/quickshell/Modules/AppDrawer/AppLauncher.qml
Normal file
168
quickshell/.config/quickshell/Modules/AppDrawer/AppLauncher.qml
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string searchQuery: ""
|
||||||
|
property string selectedCategory: "All"
|
||||||
|
property string viewMode: "list" // "list" or "grid"
|
||||||
|
property int selectedIndex: 0
|
||||||
|
property int maxResults: 50
|
||||||
|
property int gridColumns: 4
|
||||||
|
property bool debounceSearch: true
|
||||||
|
property int debounceInterval: 50
|
||||||
|
property bool keyboardNavigationActive: false
|
||||||
|
readonly property var categories: {
|
||||||
|
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
||||||
|
const result = ["All"]
|
||||||
|
return result.concat(allCategories.filter(cat => cat !== "All"))
|
||||||
|
}
|
||||||
|
readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
|
||||||
|
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
||||||
|
property alias model: filteredModel
|
||||||
|
property var _watchApplications: AppSearchService.applications
|
||||||
|
|
||||||
|
signal appLaunched(var app)
|
||||||
|
signal categorySelected(string category)
|
||||||
|
signal viewModeSelected(string mode)
|
||||||
|
|
||||||
|
function updateFilteredModel() {
|
||||||
|
filteredModel.clear()
|
||||||
|
selectedIndex = 0
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
|
||||||
|
let apps = []
|
||||||
|
if (searchQuery.length === 0) {
|
||||||
|
apps = selectedCategory === "All" ? AppSearchService.getAppsInCategory("All") : AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
|
||||||
|
} else {
|
||||||
|
if (selectedCategory === "All") {
|
||||||
|
apps = AppSearchService.searchApplications(searchQuery)
|
||||||
|
} else {
|
||||||
|
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
|
||||||
|
if (categoryApps.length > 0) {
|
||||||
|
const allSearchResults = AppSearchService.searchApplications(searchQuery)
|
||||||
|
const categoryNames = new Set(categoryApps.map(app => app.name))
|
||||||
|
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
|
||||||
|
} else {
|
||||||
|
apps = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery.length === 0) {
|
||||||
|
apps = apps.sort((a, b) => {
|
||||||
|
const aId = a.id || a.execString || a.exec || ""
|
||||||
|
const bId = b.id || b.execString || b.exec || ""
|
||||||
|
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
|
||||||
|
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
|
||||||
|
if (aUsage !== bUsage) {
|
||||||
|
return bUsage - aUsage
|
||||||
|
}
|
||||||
|
return (a.name || "").localeCompare(b.name || "")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
apps.forEach(app => {
|
||||||
|
if (app) {
|
||||||
|
filteredModel.append({
|
||||||
|
"name": app.name || "",
|
||||||
|
"exec": app.execString || "",
|
||||||
|
"icon": app.icon || "application-x-executable",
|
||||||
|
"comment": app.comment || "",
|
||||||
|
"categories": app.categories || [],
|
||||||
|
"desktopEntry": app
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNext() {
|
||||||
|
if (filteredModel.count === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrevious() {
|
||||||
|
if (filteredModel.count === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNextInRow() {
|
||||||
|
if (filteredModel.count === 0 || viewMode !== "grid") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPreviousInRow() {
|
||||||
|
if (filteredModel.count === 0 || viewMode !== "grid") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchSelected() {
|
||||||
|
if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selectedApp = filteredModel.get(selectedIndex)
|
||||||
|
launchApp(selectedApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchApp(appData) {
|
||||||
|
if (!appData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
appData.desktopEntry.execute()
|
||||||
|
appLaunched(appData)
|
||||||
|
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCategory(category) {
|
||||||
|
selectedCategory = category
|
||||||
|
categorySelected(category)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setViewMode(mode) {
|
||||||
|
viewMode = mode
|
||||||
|
viewModeSelected(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchQueryChanged: {
|
||||||
|
if (debounceSearch) {
|
||||||
|
searchDebounceTimer.restart()
|
||||||
|
} else {
|
||||||
|
updateFilteredModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSelectedCategoryChanged: updateFilteredModel()
|
||||||
|
onAppUsageRankingChanged: updateFilteredModel()
|
||||||
|
on_WatchApplicationsChanged: updateFilteredModel()
|
||||||
|
Component.onCompleted: {
|
||||||
|
updateFilteredModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: filteredModel
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: searchDebounceTimer
|
||||||
|
|
||||||
|
interval: root.debounceInterval
|
||||||
|
repeat: false
|
||||||
|
onTriggered: updateFilteredModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var categories: []
|
||||||
|
property string selectedCategory: "All"
|
||||||
|
property bool compact: false
|
||||||
|
|
||||||
|
signal categorySelected(string category)
|
||||||
|
|
||||||
|
readonly property int maxCompactItems: 8
|
||||||
|
readonly property int itemHeight: 36
|
||||||
|
readonly property color selectedBorderColor: "transparent"
|
||||||
|
readonly property color unselectedBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
|
|
||||||
|
function handleCategoryClick(category) {
|
||||||
|
selectedCategory = category
|
||||||
|
categorySelected(category)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getButtonWidth(itemCount, containerWidth) {
|
||||||
|
return itemCount > 0 ? (containerWidth - (itemCount - 1) * Theme.spacingS) / itemCount : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
height: compact ? itemHeight : (itemHeight * 2 + Theme.spacingS)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
visible: compact
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: categories ? categories.slice(0, Math.min(categories.length || 0, maxCompactItems)) : []
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property int itemCount: Math.min(categories ? categories.length || 0 : 0, maxCompactItems)
|
||||||
|
|
||||||
|
height: root.itemHeight
|
||||||
|
width: root.getButtonWidth(itemCount, parent.width)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||||
|
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData
|
||||||
|
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.handleCategoryClick(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
visible: !compact
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: categories ? categories.slice(0, Math.min(4, categories.length || 0)) : []
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property int itemCount: Math.min(4, categories ? categories.length || 0 : 0)
|
||||||
|
|
||||||
|
height: root.itemHeight
|
||||||
|
width: root.getButtonWidth(itemCount, parent.width)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||||
|
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData
|
||||||
|
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.handleCategoryClick(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: categories && categories.length > 4
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: categories && categories.length > 4 ? categories.slice(4) : []
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property int itemCount: categories && categories.length > 4 ? categories.length - 4 : 0
|
||||||
|
|
||||||
|
height: root.itemHeight
|
||||||
|
width: root.getButtonWidth(itemCount, parent.width)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||||
|
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData
|
||||||
|
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.handleCategoryClick(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,750 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.ControlCenter
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Details
|
||||||
|
import qs.Modules.ControlCenter.Details 1.0 as Details
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankPopout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string expandedSection: ""
|
||||||
|
property bool powerOptionsExpanded: false
|
||||||
|
property string triggerSection: "right"
|
||||||
|
property var triggerScreen: null
|
||||||
|
|
||||||
|
function setTriggerPosition(x, y, width, section, screen) {
|
||||||
|
triggerX = x
|
||||||
|
triggerY = y
|
||||||
|
triggerWidth = width
|
||||||
|
triggerSection = section
|
||||||
|
triggerScreen = screen
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWithSection(section) {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
expandedSection = section
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSection(section) {
|
||||||
|
if (expandedSection === section) {
|
||||||
|
expandedSection = ""
|
||||||
|
} else {
|
||||||
|
expandedSection = section
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signal powerActionRequested(string action, string title, string message)
|
||||||
|
signal lockRequested
|
||||||
|
|
||||||
|
popupWidth: 550
|
||||||
|
popupHeight: Math.min((triggerScreen?.height ?? 1080) - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400)
|
||||||
|
triggerX: (triggerScreen?.width ?? 1920) - 600 - Theme.spacingL
|
||||||
|
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
||||||
|
triggerWidth: 80
|
||||||
|
positioning: "center"
|
||||||
|
screen: triggerScreen
|
||||||
|
shouldBeVisible: false
|
||||||
|
visible: shouldBeVisible
|
||||||
|
|
||||||
|
onShouldBeVisibleChanged: {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled
|
||||||
|
if (UserInfoService)
|
||||||
|
UserInfoService.getUptime()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
NetworkService.autoRefreshEnabled = false
|
||||||
|
if (BluetoothService.adapter
|
||||||
|
&& BluetoothService.adapter.discovering)
|
||||||
|
BluetoothService.adapter.discovering = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Rectangle {
|
||||||
|
id: controlContent
|
||||||
|
|
||||||
|
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
|
||||||
|
property alias bluetoothCodecSelector: bluetoothCodecSelector
|
||||||
|
|
||||||
|
color: {
|
||||||
|
const transparency = Theme.popupTransparency || 0.92
|
||||||
|
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1)
|
||||||
|
return Qt.rgba(surface.r, surface.g, surface.b, transparency)
|
||||||
|
}
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||||
|
Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
antialiasing: true
|
||||||
|
smooth: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
width: parent.width - Theme.spacingL * 2
|
||||||
|
x: Theme.spacingL
|
||||||
|
y: Theme.spacingL
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 90
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
Theme.getContentBackgroundAlpha() * 0.4)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||||
|
Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.rightMargin: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: avatarContainer
|
||||||
|
|
||||||
|
property bool hasImage: profileImageLoader.status === Image.Ready
|
||||||
|
|
||||||
|
width: 64
|
||||||
|
height: 64
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 1
|
||||||
|
visible: parent.hasImage
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: profileImageLoader
|
||||||
|
|
||||||
|
source: {
|
||||||
|
if (PortalService.profileImage === "")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if (PortalService.profileImage.startsWith(
|
||||||
|
"/"))
|
||||||
|
return "file://" + PortalService.profileImage
|
||||||
|
|
||||||
|
return PortalService.profileImage
|
||||||
|
}
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
mipmap: true
|
||||||
|
cache: true
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiEffect {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 5
|
||||||
|
source: profileImageLoader
|
||||||
|
maskEnabled: true
|
||||||
|
maskSource: circularMask
|
||||||
|
visible: avatarContainer.hasImage
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: circularMask
|
||||||
|
|
||||||
|
width: 64 - 10
|
||||||
|
height: 64 - 10
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: "black"
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: Theme.primary
|
||||||
|
visible: !parent.hasImage
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "person"
|
||||||
|
size: Theme.iconSize + 8
|
||||||
|
color: Theme.primaryText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "warning"
|
||||||
|
size: Theme.iconSize + 8
|
||||||
|
color: Theme.primaryText
|
||||||
|
visible: PortalService.profileImage !== ""
|
||||||
|
&& profileImageLoader.status === Image.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: UserInfoService.fullName
|
||||||
|
|| UserInfoService.username || "User"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: (UserInfoService.uptime
|
||||||
|
|| "Unknown")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.rightMargin: Theme.spacingL
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 40
|
||||||
|
iconName: "lock"
|
||||||
|
iconSize: Theme.iconSize - 2
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
backgroundColor: Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.5)
|
||||||
|
onClicked: {
|
||||||
|
root.close()
|
||||||
|
root.lockRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 40
|
||||||
|
iconName: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
|
||||||
|
iconSize: Theme.iconSize - 2
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
backgroundColor: Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.5)
|
||||||
|
onClicked: {
|
||||||
|
root.powerOptionsExpanded = !root.powerOptionsExpanded
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 40
|
||||||
|
iconName: "settings"
|
||||||
|
iconSize: Theme.iconSize - 2
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
backgroundColor: Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.5)
|
||||||
|
onClicked: {
|
||||||
|
root.close()
|
||||||
|
settingsModal.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: root.powerOptionsExpanded ? 60 : 0
|
||||||
|
height: implicitHeight
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
Theme.getContentBackgroundAlpha() * 0.4)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||||
|
Theme.outline.b, 0.08)
|
||||||
|
border.width: root.powerOptionsExpanded ? 1 : 0
|
||||||
|
opacity: root.powerOptionsExpanded ? 1 : 0
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
visible: root.powerOptionsExpanded
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 100
|
||||||
|
height: 34
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: logoutButton.containsMouse ? Qt.rgba(
|
||||||
|
Theme.primary.r,
|
||||||
|
Theme.primary.g,
|
||||||
|
Theme.primary.b,
|
||||||
|
0.12) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.5)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "logout"
|
||||||
|
size: Theme.fontSizeSmall
|
||||||
|
color: logoutButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Logout"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: logoutButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: logoutButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onPressed: {
|
||||||
|
root.powerOptionsExpanded = false
|
||||||
|
root.close()
|
||||||
|
root.powerActionRequested(
|
||||||
|
"logout", "Logout",
|
||||||
|
"Are you sure you want to logout?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 100
|
||||||
|
height: 34
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: rebootButton.containsMouse ? Qt.rgba(
|
||||||
|
Theme.primary.r,
|
||||||
|
Theme.primary.g,
|
||||||
|
Theme.primary.b,
|
||||||
|
0.12) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.5)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "restart_alt"
|
||||||
|
size: Theme.fontSizeSmall
|
||||||
|
color: rebootButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Restart"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: rebootButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rebootButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onPressed: {
|
||||||
|
root.powerOptionsExpanded = false
|
||||||
|
root.close()
|
||||||
|
root.powerActionRequested(
|
||||||
|
"reboot", "Restart",
|
||||||
|
"Are you sure you want to restart?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 100
|
||||||
|
height: 34
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: suspendButton.containsMouse ? Qt.rgba(
|
||||||
|
Theme.primary.r,
|
||||||
|
Theme.primary.g,
|
||||||
|
Theme.primary.b,
|
||||||
|
0.12) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.5)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "bedtime"
|
||||||
|
size: Theme.fontSizeSmall
|
||||||
|
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Suspend"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: suspendButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onPressed: {
|
||||||
|
root.powerOptionsExpanded = false
|
||||||
|
root.close()
|
||||||
|
root.powerActionRequested(
|
||||||
|
"suspend", "Suspend",
|
||||||
|
"Are you sure you want to suspend?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 100
|
||||||
|
height: 34
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: shutdownButton.containsMouse ? Qt.rgba(
|
||||||
|
Theme.primary.r,
|
||||||
|
Theme.primary.g,
|
||||||
|
Theme.primary.b,
|
||||||
|
0.12) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.5)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "power_settings_new"
|
||||||
|
size: Theme.fontSizeSmall
|
||||||
|
color: shutdownButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Shutdown"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: shutdownButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: shutdownButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onPressed: {
|
||||||
|
root.powerOptionsExpanded = false
|
||||||
|
root.close()
|
||||||
|
root.powerActionRequested(
|
||||||
|
"poweroff", "Shutdown",
|
||||||
|
"Are you sure you want to shutdown?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: audioSliderRow.implicitHeight
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: audioSliderRow
|
||||||
|
x: -Theme.spacingS
|
||||||
|
width: parent.width + Theme.spacingS * 2
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
AudioSliderRow {
|
||||||
|
width: SettingsData.hideBrightnessSlider ? parent.width - Theme.spacingM : (parent.width - Theme.spacingM) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
|
height: parent.height
|
||||||
|
visible: !SettingsData.hideBrightnessSlider
|
||||||
|
|
||||||
|
BrightnessSliderRow {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
x: -Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
NetworkPill {
|
||||||
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
|
expanded: root.expandedSection === "network"
|
||||||
|
onClicked: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
if (NetworkService.ethernetConnected && !NetworkService.wifiEnabled) {
|
||||||
|
NetworkService.toggleWifiRadio()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root.toggleSection("network")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
if (NetworkService.ethernetConnected) {
|
||||||
|
NetworkService.toggleWifiRadio()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
NetworkService.disconnectWifi()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!NetworkService.wifiEnabled) {
|
||||||
|
NetworkService.toggleWifiRadio()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled && NetworkService.networkStatus === "disconnected") {
|
||||||
|
root.toggleSection("network")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExpandClicked: root.toggleSection("network")
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothPill {
|
||||||
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
|
expanded: root.expandedSection === "bluetooth"
|
||||||
|
onClicked: {
|
||||||
|
if (BluetoothService.adapter)
|
||||||
|
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
|
||||||
|
}
|
||||||
|
onExpandClicked: root.toggleSection("bluetooth")
|
||||||
|
visible: BluetoothService.available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
active: root.expandedSection === "network" || root.expandedSection === "bluetooth"
|
||||||
|
visible: active
|
||||||
|
sourceComponent: DetailView {
|
||||||
|
width: parent.width
|
||||||
|
isVisible: true
|
||||||
|
title: {
|
||||||
|
switch (root.expandedSection) {
|
||||||
|
case "network": return "Network Settings"
|
||||||
|
case "bluetooth": return "Bluetooth Settings"
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content: {
|
||||||
|
switch (root.expandedSection) {
|
||||||
|
case "network": return networkDetailComponent
|
||||||
|
case "bluetooth": return bluetoothDetailComponent
|
||||||
|
default: return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentHeight: 250
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
AudioOutputPill {
|
||||||
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
|
expanded: root.expandedSection === "audio_output"
|
||||||
|
onClicked: {
|
||||||
|
if (AudioService.sink) {
|
||||||
|
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExpandClicked: root.toggleSection("audio_output")
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioInputPill {
|
||||||
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
|
expanded: root.expandedSection === "audio_input"
|
||||||
|
onClicked: {
|
||||||
|
if (AudioService.source) {
|
||||||
|
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExpandClicked: root.toggleSection("audio_input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
active: root.expandedSection === "audio_output" || root.expandedSection === "audio_input"
|
||||||
|
visible: active
|
||||||
|
sourceComponent: DetailView {
|
||||||
|
width: parent.width
|
||||||
|
isVisible: true
|
||||||
|
title: {
|
||||||
|
switch (root.expandedSection) {
|
||||||
|
case "audio_output": return "Audio Output"
|
||||||
|
case "audio_input": return "Audio Input"
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content: {
|
||||||
|
switch (root.expandedSection) {
|
||||||
|
case "audio_output": return audioOutputDetailComponent
|
||||||
|
case "audio_input": return audioInputDetailComponent
|
||||||
|
default: return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentHeight: 250
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
ToggleButton {
|
||||||
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
|
iconName: DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
|
||||||
|
text: "Night Mode"
|
||||||
|
secondaryText: SessionData.nightModeAutoEnabled ? "Auto" : (DisplayService.nightModeEnabled ? "On" : "Off")
|
||||||
|
isActive: DisplayService.nightModeEnabled
|
||||||
|
enabled: DisplayService.automationAvailable
|
||||||
|
onClicked: DisplayService.toggleNightMode()
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
name: "schedule"
|
||||||
|
size: 12
|
||||||
|
color: Theme.primary
|
||||||
|
visible: SessionData.nightModeAutoEnabled
|
||||||
|
opacity: 0.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleButton {
|
||||||
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
|
iconName: SessionData.isLightMode ? "light_mode" : "palette"
|
||||||
|
text: "Theme"
|
||||||
|
secondaryText: SessionData.isLightMode ? "Light" : "Dark"
|
||||||
|
isActive: true
|
||||||
|
onClicked: Theme.toggleLightMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Details.BluetoothCodecSelector {
|
||||||
|
id: bluetoothCodecSelector
|
||||||
|
anchors.fill: parent
|
||||||
|
z: 10000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: networkDetailComponent
|
||||||
|
NetworkDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: bluetoothDetailComponent
|
||||||
|
BluetoothDetail {
|
||||||
|
id: bluetoothDetail
|
||||||
|
onShowCodecSelector: function(device) {
|
||||||
|
if (contentLoader.item && contentLoader.item.bluetoothCodecSelector) {
|
||||||
|
contentLoader.item.bluetoothCodecSelector.show(device)
|
||||||
|
contentLoader.item.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) {
|
||||||
|
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioOutputDetailComponent
|
||||||
|
AudioOutputDetail {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioInputDetailComponent
|
||||||
|
AudioInputDetail {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: headerRow.height + volumeSlider.height + audioContent.height + Theme.spacingM
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Input Devices"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
id: volumeSlider
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingXS
|
||||||
|
height: 35
|
||||||
|
value: AudioService.source && AudioService.source.audio ? Math.round(AudioService.source.audio.volume * 100) : 0
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
leftIcon: {
|
||||||
|
if (!AudioService.source || !AudioService.source.audio) return "mic_off"
|
||||||
|
let muted = AudioService.source.audio.muted
|
||||||
|
return muted ? "mic_off" : "mic"
|
||||||
|
}
|
||||||
|
rightIcon: "volume_up"
|
||||||
|
enabled: AudioService.source && AudioService.source.audio
|
||||||
|
unit: "%"
|
||||||
|
showValue: true
|
||||||
|
visible: AudioService.source && AudioService.source.audio
|
||||||
|
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (AudioService.source && AudioService.source.audio) {
|
||||||
|
AudioService.source.audio.volume = newValue / 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: parent.height
|
||||||
|
onClicked: {
|
||||||
|
if (AudioService.source && AudioService.source.audio) {
|
||||||
|
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: audioContent
|
||||||
|
anchors.top: volumeSlider.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
contentHeight: audioColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: audioColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Pipewire.nodes.values.filter(node => {
|
||||||
|
return node.audio && !node.isSink && !node.isStream
|
||||||
|
})
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||||
|
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: modelData === AudioService.source ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
if (modelData.name.includes("bluez"))
|
||||||
|
return "headset"
|
||||||
|
else if (modelData.name.includes("usb"))
|
||||||
|
return "headset"
|
||||||
|
else
|
||||||
|
return "mic"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: AudioService.displayName(modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData === AudioService.source ? "Active" : "Available"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData) {
|
||||||
|
Pipewire.preferredDefaultAudioSource = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: headerRow.height + audioContent.height + Theme.spacingM
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Audio Devices"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: audioContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
contentHeight: audioColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: audioColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Pipewire.nodes.values.filter(node => {
|
||||||
|
return node.audio && node.isSink && !node.isStream
|
||||||
|
})
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||||
|
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: modelData === AudioService.sink ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
if (modelData.name.includes("bluez"))
|
||||||
|
return "headset"
|
||||||
|
else if (modelData.name.includes("hdmi"))
|
||||||
|
return "tv"
|
||||||
|
else if (modelData.name.includes("usb"))
|
||||||
|
return "headset"
|
||||||
|
else
|
||||||
|
return "speaker"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: AudioService.displayName(modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData === AudioService.sink ? "Active" : "Available"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData) {
|
||||||
|
Pipewire.preferredDefaultAudioSink = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,307 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var device: null
|
||||||
|
property bool modalVisible: false
|
||||||
|
property var parentItem
|
||||||
|
property var availableCodecs: []
|
||||||
|
property string currentCodec: ""
|
||||||
|
property bool isLoading: false
|
||||||
|
|
||||||
|
signal codecSelected(string deviceAddress, string codecName)
|
||||||
|
|
||||||
|
function show(bluetoothDevice) {
|
||||||
|
device = bluetoothDevice;
|
||||||
|
isLoading = true;
|
||||||
|
availableCodecs = [];
|
||||||
|
currentCodec = "";
|
||||||
|
visible = true;
|
||||||
|
modalVisible = true;
|
||||||
|
queryCodecs();
|
||||||
|
Qt.callLater(() => {
|
||||||
|
focusScope.forceActiveFocus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modalVisible = false;
|
||||||
|
Qt.callLater(() => {
|
||||||
|
visible = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryCodecs() {
|
||||||
|
if (!device)
|
||||||
|
return;
|
||||||
|
|
||||||
|
BluetoothService.getAvailableCodecs(device, function(codecs, current) {
|
||||||
|
availableCodecs = codecs;
|
||||||
|
currentCodec = current;
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCodec(profileName) {
|
||||||
|
if (!device || isLoading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let selectedCodec = availableCodecs.find(c => c.profile === profileName);
|
||||||
|
if (selectedCodec && device) {
|
||||||
|
BluetoothService.updateDeviceCodec(device.address, selectedCodec.name);
|
||||||
|
codecSelected(device.address, selectedCodec.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
BluetoothService.switchCodec(device, profileName, function(success, message) {
|
||||||
|
isLoading = false;
|
||||||
|
if (success) {
|
||||||
|
ToastService.showToast(message, ToastService.levelInfo);
|
||||||
|
Qt.callLater(root.hide);
|
||||||
|
} else {
|
||||||
|
ToastService.showToast(message, ToastService.levelError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
anchors.fill: parent
|
||||||
|
z: 2000
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: modalBlocker
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: modalVisible
|
||||||
|
enabled: modalVisible
|
||||||
|
hoverEnabled: true
|
||||||
|
preventStealing: true
|
||||||
|
propagateComposedEvents: false
|
||||||
|
|
||||||
|
onClicked: root.hide()
|
||||||
|
onWheel: (wheel) => { wheel.accepted = true }
|
||||||
|
onPositionChanged: (mouse) => { mouse.accepted = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: modalBackground
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.5)
|
||||||
|
opacity: modalVisible ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: focusScope
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: root.visible
|
||||||
|
enabled: root.visible
|
||||||
|
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
root.hide()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: modalContent
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 320
|
||||||
|
height: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
opacity: modalVisible ? 1 : 0
|
||||||
|
scale: modalVisible ? 1 : 0.9
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
preventStealing: true
|
||||||
|
propagateComposedEvents: false
|
||||||
|
onClicked: (mouse) => { mouse.accepted = true }
|
||||||
|
onWheel: (wheel) => { wheel.accepted = true }
|
||||||
|
onPositionChanged: (mouse) => { mouse.accepted = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: contentColumn
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: device ? BluetoothService.getDeviceIcon(device) : "headset"
|
||||||
|
size: Theme.iconSize + 4
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: device ? (device.name || device.deviceName) : ""
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Audio Codec Selection"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: isLoading ? "Loading codecs..." : `Current: ${currentCodec}`
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: isLoading ? Theme.primary : Theme.surfaceTextMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: !isLoading
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: availableCodecs
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (modelData.name === currentCodec)
|
||||||
|
return Theme.surfaceContainerHigh;
|
||||||
|
else if (codecMouseArea.containsMouse)
|
||||||
|
return Theme.surfaceHover;
|
||||||
|
else
|
||||||
|
return "transparent";
|
||||||
|
}
|
||||||
|
border.color: "transparent"
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 6
|
||||||
|
height: 6
|
||||||
|
radius: 3
|
||||||
|
color: modelData.qualityColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.name
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: modelData.name === currentCodec ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: modelData.name === currentCodec ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.description
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "check"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: modelData.name === currentCodec
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: codecMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: modelData.name !== currentCodec && !isLoading
|
||||||
|
onClicked: {
|
||||||
|
selectCodec(modelData.profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,543 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Bluetooth
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: BluetoothService.adapter && BluetoothService.adapter.enabled ? headerRow.height + bluetoothContent.height + Theme.spacingM : headerRow.height
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
property var bluetoothCodecModalRef: null
|
||||||
|
|
||||||
|
signal showCodecSelector(var device)
|
||||||
|
|
||||||
|
function updateDeviceCodecDisplay(deviceAddress, codecName) {
|
||||||
|
for (let i = 0; i < pairedRepeater.count; i++) {
|
||||||
|
let item = pairedRepeater.itemAt(i)
|
||||||
|
if (item && item.modelData && item.modelData.address === deviceAddress) {
|
||||||
|
item.currentCodec = codecName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Bluetooth Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Math.max(0, parent.width - headerText.implicitWidth - scanButton.width - Theme.spacingM)
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: scanButton
|
||||||
|
width: 100
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
return scanMouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
|
||||||
|
}
|
||||||
|
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
|
||||||
|
size: 18
|
||||||
|
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Scanning" : "Scan"
|
||||||
|
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: scanMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: {
|
||||||
|
if (BluetoothService.adapter)
|
||||||
|
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: bluetoothContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
|
contentHeight: bluetoothColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: bluetoothColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: pairedRepeater
|
||||||
|
model: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||||
|
return []
|
||||||
|
|
||||||
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
|
devices.sort((a, b) => {
|
||||||
|
if (a.connected && !b.connected) return -1
|
||||||
|
if (!a.connected && b.connected) return 1
|
||||||
|
return (b.signalStrength || 0) - (a.signalStrength || 0)
|
||||||
|
})
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
property string currentCodec: BluetoothService.deviceCodecs[modelData.address] || ""
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (modelData.connected && BluetoothService.isAudioDevice(modelData)) {
|
||||||
|
BluetoothService.refreshDeviceCodec(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
|
||||||
|
if (deviceMouseArea.containsMouse)
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||||
|
}
|
||||||
|
border.color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Theme.warning
|
||||||
|
if (modelData.connected)
|
||||||
|
return Theme.primary
|
||||||
|
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
border.width: (modelData.connected || modelData.state === BluetoothDeviceState.Connecting) ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: BluetoothService.getDeviceIcon(modelData)
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Theme.warning
|
||||||
|
if (modelData.connected)
|
||||||
|
return Theme.primary
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 200
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.name || modelData.deviceName || "Unknown Device"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return "Connecting..."
|
||||||
|
if (modelData.connected) {
|
||||||
|
let status = "Connected"
|
||||||
|
if (currentCodec) {
|
||||||
|
status += " • " + currentCodec
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
return "Paired"
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Theme.warning
|
||||||
|
return Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (modelData.batteryAvailable && modelData.battery > 0)
|
||||||
|
return "• " + Math.round(modelData.battery * 100) + "%"
|
||||||
|
|
||||||
|
var btBattery = BatteryService.bluetoothDevices.find(dev => {
|
||||||
|
return dev.name === (modelData.name || modelData.deviceName) ||
|
||||||
|
dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) ||
|
||||||
|
(modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase())
|
||||||
|
})
|
||||||
|
return btBattery ? "• " + btBattery.percentage + "%" : ""
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: pairedOptionsButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "more_horiz"
|
||||||
|
buttonSize: 28
|
||||||
|
onClicked: {
|
||||||
|
if (bluetoothContextMenu.visible) {
|
||||||
|
bluetoothContextMenu.close()
|
||||||
|
} else {
|
||||||
|
bluetoothContextMenu.currentDevice = modelData
|
||||||
|
bluetoothContextMenu.popup(pairedOptionsButton, -bluetoothContextMenu.width + pairedOptionsButton.width, pairedOptionsButton.height + Theme.spacingXS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: pairedOptionsButton.width + Theme.spacingS
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.connected) {
|
||||||
|
modelData.disconnect()
|
||||||
|
} else {
|
||||||
|
BluetoothService.connectDeviceWithTrust(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
visible: pairedRepeater.count > 0 && availableRepeater.count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 80
|
||||||
|
visible: BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "sync"
|
||||||
|
size: 24
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4)
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: parent.visible && BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: availableRepeater
|
||||||
|
model: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||||
|
return []
|
||||||
|
|
||||||
|
var filtered = Bluetooth.devices.values.filter(dev => {
|
||||||
|
return dev && !dev.paired && !dev.pairing && !dev.blocked &&
|
||||||
|
(dev.signalStrength === undefined || dev.signalStrength > 0)
|
||||||
|
})
|
||||||
|
return BluetoothService.sortDevices(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
property bool canConnect: BluetoothService.canConnect(modelData)
|
||||||
|
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: availableMouseArea.containsMouse && !isBusy ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.15)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
opacity: canConnect ? 1 : 0.6
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: BluetoothService.getDeviceIcon(modelData)
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 200
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.name || modelData.deviceName || "Unknown Device"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (modelData.pairing) return "Pairing..."
|
||||||
|
if (modelData.blocked) return "Blocked"
|
||||||
|
return BluetoothService.getSignalStrength(modelData)
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0 && !modelData.pairing && !modelData.blocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: {
|
||||||
|
if (modelData.pairing) return "Pairing..."
|
||||||
|
if (!canConnect) return "Cannot pair"
|
||||||
|
return "Pair"
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: canConnect ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: availableMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
enabled: canConnect && !isBusy
|
||||||
|
onClicked: {
|
||||||
|
if (modelData) {
|
||||||
|
BluetoothService.connectDeviceWithTrust(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
visible: !BluetoothService.adapter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "No Bluetooth adapter found"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: bluetoothContextMenu
|
||||||
|
width: 150
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||||
|
|
||||||
|
property var currentDevice: null
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: bluetoothContextMenu.currentDevice && bluetoothContextMenu.currentDevice.connected ? "Disconnect" : "Connect"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (bluetoothContextMenu.currentDevice) {
|
||||||
|
if (bluetoothContextMenu.currentDevice.connected) {
|
||||||
|
bluetoothContextMenu.currentDevice.disconnect()
|
||||||
|
} else {
|
||||||
|
BluetoothService.connectDeviceWithTrust(bluetoothContextMenu.currentDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Audio Codec"
|
||||||
|
height: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected ? 32 : 0
|
||||||
|
visible: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (bluetoothContextMenu.currentDevice) {
|
||||||
|
showCodecSelector(bluetoothContextMenu.currentDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Forget Device"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (bluetoothContextMenu.currentDevice) {
|
||||||
|
bluetoothContextMenu.currentDevice.forget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,533 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return headerRow.height + wifiToggleContent.height + Theme.spacingM
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return headerRow.height + wifiContent.height + Theme.spacingM
|
||||||
|
}
|
||||||
|
return headerRow.height + wifiOffContent.height + Theme.spacingM
|
||||||
|
}
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
NetworkService.addRef()
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
NetworkService.scanWifi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
NetworkService.removeRef()
|
||||||
|
}
|
||||||
|
|
||||||
|
property var wifiPasswordModalRef: {
|
||||||
|
wifiPasswordModalLoader.active = true
|
||||||
|
return wifiPasswordModalLoader.item
|
||||||
|
}
|
||||||
|
property var networkInfoModalRef: {
|
||||||
|
networkInfoModalLoader.active = true
|
||||||
|
return networkInfoModalLoader.item
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Network Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Math.max(0, parent.width - headerText.implicitWidth - preferenceControls.width - Theme.spacingM)
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: preferenceControls
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: NetworkService.ethernetConnected && NetworkService.wifiConnected
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isActive: NetworkService.userPreference === "ethernet"
|
||||||
|
|
||||||
|
width: 90
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: isActive ? Theme.surfaceContainerHigh : ethernetMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Ethernet"
|
||||||
|
color: parent.isActive ? Theme.primary : Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: parent.isActive ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: ethernetMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: NetworkService.setNetworkPreference("ethernet")
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isActive: NetworkService.userPreference === "wifi"
|
||||||
|
|
||||||
|
width: 70
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: isActive ? Theme.surfaceContainerHigh : wifiMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "WiFi"
|
||||||
|
color: parent.isActive ? Theme.primary : Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: parent.isActive ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: wifiMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: NetworkService.setNetworkPreference("wifi")
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: wifiToggleContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: NetworkService.wifiToggling
|
||||||
|
height: visible ? 80 : 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
name: "sync"
|
||||||
|
size: 32
|
||||||
|
color: Theme.primary
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: NetworkService.wifiToggling
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: wifiOffContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: !NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
||||||
|
height: visible ? 120 : 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
name: "wifi_off"
|
||||||
|
size: 48
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "WiFi is off"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: 120
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: enableWifiButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primary
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Enable WiFi"
|
||||||
|
color: Theme.primary
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: enableWifiButton
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: NetworkService.toggleWifiRadio()
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: wifiContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: NetworkService.wifiInterface && NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
||||||
|
contentHeight: wifiColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: wifiColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 200
|
||||||
|
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "refresh"
|
||||||
|
size: 48
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: true
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: {
|
||||||
|
let networks = [...NetworkService.wifiNetworks]
|
||||||
|
networks.sort((a, b) => {
|
||||||
|
if (a.ssid === NetworkService.currentWifiSSID) return -1
|
||||||
|
if (b.ssid === NetworkService.currentWifiSSID) return 1
|
||||||
|
return b.signal - a.signal
|
||||||
|
})
|
||||||
|
return networks
|
||||||
|
}
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||||
|
border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: modelData.ssid === NetworkService.currentWifiSSID ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
let strength = modelData.signal || 0
|
||||||
|
if (strength >= 70) return "signal_wifi_4_bar"
|
||||||
|
if (strength >= 50) return "network_wifi_3_bar"
|
||||||
|
if (strength >= 25) return "network_wifi_2_bar"
|
||||||
|
if (strength >= 10) return "network_wifi_1_bar"
|
||||||
|
return "signal_wifi_bad"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 200
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.ssid || "Unknown Network"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.saved ? "• Saved" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.primary
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "• " + modelData.signal + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: optionsButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "more_horiz"
|
||||||
|
buttonSize: 28
|
||||||
|
onClicked: {
|
||||||
|
if (networkContextMenu.visible) {
|
||||||
|
networkContextMenu.close()
|
||||||
|
} else {
|
||||||
|
networkContextMenu.currentSSID = modelData.ssid
|
||||||
|
networkContextMenu.currentSecured = modelData.secured
|
||||||
|
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID
|
||||||
|
networkContextMenu.currentSaved = modelData.saved
|
||||||
|
networkContextMenu.currentSignal = modelData.signal
|
||||||
|
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: networkMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: optionsButton.width + Theme.spacingS
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: function(event) {
|
||||||
|
if (modelData.ssid !== NetworkService.currentWifiSSID) {
|
||||||
|
if (modelData.secured && !modelData.saved) {
|
||||||
|
if (wifiPasswordModalRef) {
|
||||||
|
wifiPasswordModalRef.show(modelData.ssid)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NetworkService.connectToWifi(modelData.ssid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: networkContextMenu
|
||||||
|
width: 150
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||||
|
|
||||||
|
property string currentSSID: ""
|
||||||
|
property bool currentSecured: false
|
||||||
|
property bool currentConnected: false
|
||||||
|
property bool currentSaved: false
|
||||||
|
property int currentSignal: 0
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: networkContextMenu.currentConnected ? "Disconnect" : "Connect"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (networkContextMenu.currentConnected) {
|
||||||
|
NetworkService.disconnectWifi()
|
||||||
|
} else {
|
||||||
|
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
|
||||||
|
if (wifiPasswordModalRef) {
|
||||||
|
wifiPasswordModalRef.show(networkContextMenu.currentSSID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NetworkService.connectToWifi(networkContextMenu.currentSSID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Network Info"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (networkInfoModalRef) {
|
||||||
|
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID)
|
||||||
|
networkInfoModalRef.showNetworkInfo(networkContextMenu.currentSSID, networkData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Forget Network"
|
||||||
|
height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0
|
||||||
|
visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: wifiPasswordModalLoader
|
||||||
|
active: false
|
||||||
|
|
||||||
|
WifiPasswordModal {
|
||||||
|
id: wifiPasswordModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: networkInfoModalLoader
|
||||||
|
active: false
|
||||||
|
|
||||||
|
NetworkInfoModal {
|
||||||
|
id: networkInfoModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool powerMenuVisible: false
|
||||||
|
signal powerActionRequested(string action, string title, string message)
|
||||||
|
|
||||||
|
visible: powerMenuVisible
|
||||||
|
implicitWidth: 400
|
||||||
|
implicitHeight: 320
|
||||||
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Math.min(320, parent.width - Theme.spacingL * 2)
|
||||||
|
height: 320 // Fixed height to prevent cropping
|
||||||
|
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
|
||||||
|
y: Theme.barHeight + Theme.spacingXS
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||||
|
Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
opacity: powerMenuVisible ? 1 : 0
|
||||||
|
scale: powerMenuVisible ? 1 : 0.85
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Power Options"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - 150
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
||||||
|
Theme.primary.g,
|
||||||
|
Theme.primary.b,
|
||||||
|
0.08) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.08)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "logout"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Log Out"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: logoutArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
root.powerActionRequested(
|
||||||
|
"logout", "Log Out",
|
||||||
|
"Are you sure you want to log out?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
||||||
|
Theme.primary.g,
|
||||||
|
Theme.primary.b,
|
||||||
|
0.08) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.08)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "bedtime"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Suspend"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: suspendArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
root.powerActionRequested(
|
||||||
|
"suspend", "Suspend",
|
||||||
|
"Are you sure you want to suspend the system?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r,
|
||||||
|
Theme.warning.g,
|
||||||
|
Theme.warning.b,
|
||||||
|
0.08) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.08)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "restart_alt"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Reboot"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rebootArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
root.powerActionRequested(
|
||||||
|
"reboot", "Reboot",
|
||||||
|
"Are you sure you want to reboot the system?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r,
|
||||||
|
Theme.error.g,
|
||||||
|
Theme.error.b,
|
||||||
|
0.08) : Qt.rgba(
|
||||||
|
Theme.surfaceVariant.r,
|
||||||
|
Theme.surfaceVariant.g,
|
||||||
|
Theme.surfaceVariant.b,
|
||||||
|
0.08)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "power_settings_new"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Power Off"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: powerOffArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
powerMenuVisible = false
|
||||||
|
root.powerActionRequested(
|
||||||
|
"poweroff", "Power Off",
|
||||||
|
"Are you sure you want to power off the system?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
BasePill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSource: AudioService.source
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!defaultSource) return "mic_off"
|
||||||
|
|
||||||
|
let volume = defaultSource.audio.volume
|
||||||
|
let muted = defaultSource.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "mic_off"
|
||||||
|
return "mic"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: defaultSource && !defaultSource.audio.muted
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!defaultSource) {
|
||||||
|
return "No input device"
|
||||||
|
}
|
||||||
|
return defaultSource.description || "Audio Input"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!defaultSource) {
|
||||||
|
return "Select device"
|
||||||
|
}
|
||||||
|
if (defaultSource.audio.muted) {
|
||||||
|
return "Muted"
|
||||||
|
}
|
||||||
|
return Math.round(defaultSource.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheelEvent: function (wheelEvent) {
|
||||||
|
if (!defaultSource || !defaultSource.audio) return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = defaultSource.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
defaultSource.audio.muted = false
|
||||||
|
defaultSource.audio.volume = newVolume / 100
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
BasePill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSink: AudioService.sink
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!defaultSink) return "volume_off"
|
||||||
|
|
||||||
|
let volume = defaultSink.audio.volume
|
||||||
|
let muted = defaultSink.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: defaultSink && !defaultSink.audio.muted
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!defaultSink) {
|
||||||
|
return "No output device"
|
||||||
|
}
|
||||||
|
return defaultSink.description || "Audio Output"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!defaultSink) {
|
||||||
|
return "Select device"
|
||||||
|
}
|
||||||
|
if (defaultSink.audio.muted) {
|
||||||
|
return "Muted"
|
||||||
|
}
|
||||||
|
return Math.round(defaultSink.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheelEvent: function (wheelEvent) {
|
||||||
|
if (!defaultSink || !defaultSink.audio) return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = defaultSink.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
defaultSink.audio.muted = false
|
||||||
|
defaultSink.audio.volume = newVolume / 100
|
||||||
|
AudioService.volumeChanged()
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
SimpleSlider {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSink: AudioService.sink
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!defaultSink) return "volume_off"
|
||||||
|
|
||||||
|
let volume = defaultSink.audio.volume
|
||||||
|
let muted = defaultSink.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
|
||||||
|
iconColor: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
|
||||||
|
enabled: defaultSink !== null
|
||||||
|
allowIconClick: defaultSink !== null
|
||||||
|
|
||||||
|
value: defaultSink ? defaultSink.audio.volume : 0.0
|
||||||
|
maximumValue: 1.0
|
||||||
|
minimumValue: 0.0
|
||||||
|
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.volume = newValue
|
||||||
|
if (newValue > 0 && defaultSink.audio.muted) {
|
||||||
|
defaultSink.audio.muted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onIconClicked: function() {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.muted = !defaultSink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSink: AudioService.sink
|
||||||
|
|
||||||
|
height: 40
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: (Theme.iconSize + Theme.spacingS * 2) / 2 // Make it circular
|
||||||
|
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: iconArea
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: defaultSink !== null
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.muted = !defaultSink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
if (!defaultSink) return "volume_off"
|
||||||
|
|
||||||
|
let volume = defaultSink.audio.volume
|
||||||
|
let muted = defaultSink.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingM
|
||||||
|
enabled: defaultSink !== null
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
value: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.volume = newValue / 100.0
|
||||||
|
if (newValue > 0 && defaultSink.audio.muted) {
|
||||||
|
defaultSink.audio.muted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property color iconColor: Theme.surfaceText
|
||||||
|
property string primaryText: ""
|
||||||
|
property string secondaryText: ""
|
||||||
|
property bool expanded: false
|
||||||
|
property bool isActive: false
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
signal expandClicked()
|
||||||
|
signal wheelEvent(var wheelEvent)
|
||||||
|
|
||||||
|
width: parent ? parent.width : 200
|
||||||
|
height: 60
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: mainArea
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: parent.width - expandArea.width
|
||||||
|
topLeftRadius: Theme.cornerRadius
|
||||||
|
bottomLeftRadius: Theme.cornerRadius
|
||||||
|
topRightRadius: 0
|
||||||
|
bottomRightRadius: 0
|
||||||
|
color: mainAreaMouse.containsMouse ?
|
||||||
|
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||||
|
"transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: root.isActive ? Theme.primary : root.iconColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - Theme.iconSize - Theme.spacingS
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.primaryText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.secondaryText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mainAreaMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.clicked()
|
||||||
|
onWheel: function (wheelEvent) {
|
||||||
|
root.wheelEvent(wheelEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: expandArea
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: Theme.iconSize + Theme.spacingM * 2
|
||||||
|
topLeftRadius: 0
|
||||||
|
bottomLeftRadius: 0
|
||||||
|
topRightRadius: Theme.cornerRadius
|
||||||
|
bottomRightRadius: Theme.cornerRadius
|
||||||
|
color: expandAreaMouse.containsMouse ?
|
||||||
|
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||||
|
"transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: expandIcon
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: expanded ? "expand_less" : "expand_more"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: expandAreaMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.expandClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
BasePill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var primaryDevice: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
|
for (let device of devices) {
|
||||||
|
if (device && device.connected) {
|
||||||
|
return device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
}
|
||||||
|
if (primaryDevice) {
|
||||||
|
return BluetoothService.getDeviceIcon(primaryDevice)
|
||||||
|
}
|
||||||
|
return "bluetooth"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "Bluetooth unavailable"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter) {
|
||||||
|
return "No adapter"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter.enabled) {
|
||||||
|
return "Disabled"
|
||||||
|
}
|
||||||
|
return "Enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "Hardware not found"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
||||||
|
return "Off"
|
||||||
|
}
|
||||||
|
if (primaryDevice) {
|
||||||
|
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
|
||||||
|
}
|
||||||
|
return "No devices"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
SimpleSlider {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!DisplayService.brightnessAvailable) return "brightness_low"
|
||||||
|
|
||||||
|
let brightness = DisplayService.brightnessLevel
|
||||||
|
if (brightness <= 33) return "brightness_low"
|
||||||
|
if (brightness <= 66) return "brightness_medium"
|
||||||
|
return "brightness_high"
|
||||||
|
}
|
||||||
|
|
||||||
|
iconColor: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
|
||||||
|
enabled: DisplayService.brightnessAvailable
|
||||||
|
|
||||||
|
value: DisplayService.brightnessLevel
|
||||||
|
maximumValue: 100.0
|
||||||
|
minimumValue: 0.0
|
||||||
|
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (DisplayService.brightnessAvailable) {
|
||||||
|
DisplayService.brightnessLevel = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
height: 40
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||||
|
color: iconArea.containsMouse
|
||||||
|
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||||
|
: "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: iconArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: function(event) {
|
||||||
|
if (DisplayService.devices.length > 1) {
|
||||||
|
if (deviceMenu.visible) {
|
||||||
|
deviceMenu.close()
|
||||||
|
} else {
|
||||||
|
deviceMenu.popup(iconArea, 0, iconArea.height + Theme.spacingXS)
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
if (!DisplayService.brightnessAvailable) return "brightness_low"
|
||||||
|
|
||||||
|
let brightness = DisplayService.brightnessLevel
|
||||||
|
if (brightness <= 33) return "brightness_low"
|
||||||
|
if (brightness <= 66) return "brightness_medium"
|
||||||
|
return "brightness_high"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingM
|
||||||
|
enabled: DisplayService.brightnessAvailable
|
||||||
|
minimum: 1
|
||||||
|
maximum: 100
|
||||||
|
value: {
|
||||||
|
let level = DisplayService.brightnessLevel
|
||||||
|
if (level > 100) {
|
||||||
|
let deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||||
|
if (deviceInfo && deviceInfo.max > 0) {
|
||||||
|
return Math.round((level / deviceInfo.max) * 100)
|
||||||
|
}
|
||||||
|
return 50
|
||||||
|
}
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (DisplayService.brightnessAvailable) {
|
||||||
|
DisplayService.setBrightness(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: deviceMenu
|
||||||
|
width: 200
|
||||||
|
closePolicy: Popup.CloseOnEscape
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: DisplayService.devices
|
||||||
|
delegate: MenuItem {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
property string deviceName: modelData.name || ""
|
||||||
|
property string deviceClass: modelData.class || ""
|
||||||
|
|
||||||
|
text: deviceName
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
indicator: Rectangle {
|
||||||
|
visible: DisplayService.currentDevice === parent.deviceName
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
width: 4
|
||||||
|
height: parent.height - Theme.spacingS * 2
|
||||||
|
radius: 2
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font: parent.font
|
||||||
|
color: DisplayService.currentDevice === parent.deviceName ? Theme.primary : Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingL
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
DisplayService.setCurrentDevice(deviceName, true)
|
||||||
|
deviceMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onObjectAdded: (index, object) => deviceMenu.insertItem(index, object)
|
||||||
|
onObjectRemoved: (index, object) => deviceMenu.removeItem(object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property color iconColor: Theme.surfaceText
|
||||||
|
property string labelText: ""
|
||||||
|
property real value: 0.0
|
||||||
|
property real maximumValue: 1.0
|
||||||
|
property real minimumValue: 0.0
|
||||||
|
property bool enabled: true
|
||||||
|
|
||||||
|
signal sliderValueChanged(real value)
|
||||||
|
|
||||||
|
width: parent ? parent.width : 200
|
||||||
|
height: 60
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.right: sliderContainer.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: root.iconColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.labelText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: sliderContainer
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
width: 120
|
||||||
|
height: parent.height - Theme.spacingS * 2
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
enabled: root.enabled
|
||||||
|
minimum: Math.round(root.minimumValue * 100)
|
||||||
|
maximum: Math.round(root.maximumValue * 100)
|
||||||
|
value: Math.round(root.value * 100)
|
||||||
|
onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string title: ""
|
||||||
|
property Component content: null
|
||||||
|
property bool isVisible: true
|
||||||
|
property int contentHeight: 300
|
||||||
|
|
||||||
|
width: parent ? parent.width : 400
|
||||||
|
implicitHeight: isVisible ? contentHeight : 0
|
||||||
|
height: implicitHeight
|
||||||
|
color: "transparent"
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: contentLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
sourceComponent: root.content
|
||||||
|
asynchronous: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
BasePill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
isActive: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return NetworkService.wifiEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return "sync"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "settings_ethernet"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
return NetworkService.wifiSignalIcon
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "signal_wifi_off"
|
||||||
|
}
|
||||||
|
return "wifi_off"
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "Ethernet"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) {
|
||||||
|
return NetworkService.currentWifiSSID
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "Not connected"
|
||||||
|
}
|
||||||
|
return "WiFi off"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (NetworkService.wifiToggling) {
|
||||||
|
return "Please wait..."
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "Connected"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "Select network"
|
||||||
|
}
|
||||||
|
return "Tap to enable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property color iconColor: Theme.surfaceText
|
||||||
|
property real value: 0.0
|
||||||
|
property real maximumValue: 1.0
|
||||||
|
property real minimumValue: 0.0
|
||||||
|
property bool enabled: true
|
||||||
|
property bool allowIconClick: false
|
||||||
|
|
||||||
|
signal sliderValueChanged(real value)
|
||||||
|
signal iconClicked()
|
||||||
|
|
||||||
|
height: 60
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: root.iconColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.allowIconClick
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.iconClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: {
|
||||||
|
if (parent.width <= 0) return 80
|
||||||
|
return Math.max(80, Math.min(400, parent.width - Theme.iconSize - Theme.spacingM))
|
||||||
|
}
|
||||||
|
enabled: root.enabled
|
||||||
|
minimum: Math.round(root.minimumValue * 100)
|
||||||
|
maximum: Math.round(root.maximumValue * 100)
|
||||||
|
value: Math.round(root.value * 100)
|
||||||
|
onSliderValueChanged: function(newValue) { root.sliderValueChanged(newValue / 100.0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property string text: ""
|
||||||
|
property bool isActive: false
|
||||||
|
property bool enabled: true
|
||||||
|
property string secondaryText: ""
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
width: parent ? parent.width : 200
|
||||||
|
height: 60
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: mouseArea.containsMouse ?
|
||||||
|
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||||
|
"transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: root.isActive ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - Theme.iconSize - Theme.spacingS
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.text
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.secondaryText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: root.enabled
|
||||||
|
onClicked: root.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,315 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.DankDash
|
||||||
|
|
||||||
|
DankPopout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool dashVisible: false
|
||||||
|
property var triggerScreen: null
|
||||||
|
property int currentTabIndex: 0
|
||||||
|
|
||||||
|
function setTriggerPosition(x, y, width, section, screen) {
|
||||||
|
triggerScreen = screen
|
||||||
|
triggerY = y
|
||||||
|
|
||||||
|
const screenWidth = screen ? screen.width : Screen.width
|
||||||
|
triggerX = (screenWidth - popupWidth) / 2
|
||||||
|
triggerWidth = popupWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
popupWidth: 700
|
||||||
|
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
||||||
|
triggerX: Screen.width - 620 - Theme.spacingL
|
||||||
|
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
|
||||||
|
triggerWidth: 80
|
||||||
|
shouldBeVisible: dashVisible
|
||||||
|
visible: shouldBeVisible
|
||||||
|
|
||||||
|
property bool __focusArmed: false
|
||||||
|
property bool __contentReady: false
|
||||||
|
|
||||||
|
function __tryFocusOnce() {
|
||||||
|
if (!__focusArmed)
|
||||||
|
return
|
||||||
|
const win = root.window
|
||||||
|
if (!win || !win.visible)
|
||||||
|
return
|
||||||
|
if (!contentLoader.item)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (win.requestActivate)
|
||||||
|
win.requestActivate()
|
||||||
|
contentLoader.item.forceActiveFocus(Qt.TabFocusReason)
|
||||||
|
|
||||||
|
if (contentLoader.item.activeFocus)
|
||||||
|
__focusArmed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onDashVisibleChanged: {
|
||||||
|
if (dashVisible) {
|
||||||
|
__focusArmed = true
|
||||||
|
__contentReady = !!contentLoader.item
|
||||||
|
open()
|
||||||
|
__tryFocusOnce()
|
||||||
|
} else {
|
||||||
|
__focusArmed = false
|
||||||
|
__contentReady = false
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: contentLoader
|
||||||
|
function onLoaded() {
|
||||||
|
__contentReady = true
|
||||||
|
if (__focusArmed)
|
||||||
|
__tryFocusOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.window ? root.window : null
|
||||||
|
enabled: !!root.window
|
||||||
|
function onVisibleChanged() {
|
||||||
|
if (__focusArmed)
|
||||||
|
__tryFocusOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBackgroundClicked: {
|
||||||
|
dashVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Rectangle {
|
||||||
|
id: mainContainer
|
||||||
|
|
||||||
|
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (root.shouldBeVisible) {
|
||||||
|
mainContainer.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onShouldBeVisibleChanged() {
|
||||||
|
if (root.shouldBeVisible) {
|
||||||
|
Qt.callLater(function () {
|
||||||
|
mainContainer.forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
root.dashVisible = false
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) {
|
||||||
|
let nextIndex = root.currentTabIndex + 1
|
||||||
|
while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
|
||||||
|
nextIndex++
|
||||||
|
}
|
||||||
|
if (nextIndex >= tabBar.model.length) {
|
||||||
|
nextIndex = 0
|
||||||
|
}
|
||||||
|
root.currentTabIndex = nextIndex
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
|
||||||
|
let prevIndex = root.currentTabIndex - 1
|
||||||
|
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
|
||||||
|
prevIndex--
|
||||||
|
}
|
||||||
|
if (prevIndex < 0) {
|
||||||
|
prevIndex = tabBar.model.length - 1
|
||||||
|
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
|
||||||
|
prevIndex--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prevIndex >= 0) {
|
||||||
|
root.currentTabIndex = prevIndex
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
|
||||||
|
if (wallpaperTab.handleKeyEvent(event)) {
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||||
|
radius: parent.radius
|
||||||
|
|
||||||
|
SequentialAnimation on opacity {
|
||||||
|
running: root.shouldBeVisible
|
||||||
|
loops: Animation.Infinite
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
to: 0.08
|
||||||
|
duration: Theme.extraLongDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
to: 0.02
|
||||||
|
duration: Theme.extraLongDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: contentColumn
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankTabBar {
|
||||||
|
id: tabBar
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
currentIndex: root.currentTabIndex
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
equalWidthTabs: true
|
||||||
|
enableArrowNavigation: false
|
||||||
|
focus: false
|
||||||
|
activeFocusOnTab: false
|
||||||
|
nextFocusTarget: {
|
||||||
|
const item = pages.currentItem
|
||||||
|
if (!item)
|
||||||
|
return null
|
||||||
|
if (item.focusTarget)
|
||||||
|
return item.focusTarget
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
model: {
|
||||||
|
let tabs = [{
|
||||||
|
"icon": "dashboard",
|
||||||
|
"text": "Overview"
|
||||||
|
}, {
|
||||||
|
"icon": "music_note",
|
||||||
|
"text": "Media"
|
||||||
|
}, {
|
||||||
|
"icon": "wallpaper",
|
||||||
|
"text": "Wallpapers"
|
||||||
|
}]
|
||||||
|
|
||||||
|
if (SettingsData.weatherEnabled) {
|
||||||
|
tabs.push({
|
||||||
|
"icon": "wb_sunny",
|
||||||
|
"text": "Weather"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tabs.push({
|
||||||
|
"icon": "settings",
|
||||||
|
"text": "Settings",
|
||||||
|
"isAction": true
|
||||||
|
})
|
||||||
|
return tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
onTabClicked: function (index) {
|
||||||
|
root.currentTabIndex = index
|
||||||
|
}
|
||||||
|
|
||||||
|
onActionTriggered: function (index) {
|
||||||
|
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3
|
||||||
|
if (index === settingsIndex) {
|
||||||
|
dashVisible = false
|
||||||
|
settingsModal.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: Theme.spacingXS
|
||||||
|
}
|
||||||
|
|
||||||
|
StackLayout {
|
||||||
|
id: pages
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: {
|
||||||
|
if (currentIndex === 0)
|
||||||
|
return overviewTab.implicitHeight
|
||||||
|
if (currentIndex === 1)
|
||||||
|
return mediaTab.implicitHeight
|
||||||
|
if (currentIndex === 2)
|
||||||
|
return wallpaperTab.implicitHeight
|
||||||
|
if (SettingsData.weatherEnabled && currentIndex === 3)
|
||||||
|
return weatherTab.implicitHeight
|
||||||
|
return overviewTab.implicitHeight
|
||||||
|
}
|
||||||
|
currentIndex: root.currentTabIndex
|
||||||
|
|
||||||
|
OverviewTab {
|
||||||
|
id: overviewTab
|
||||||
|
|
||||||
|
onCloseDash: {
|
||||||
|
root.dashVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onSwitchToWeatherTab: {
|
||||||
|
if (SettingsData.weatherEnabled) {
|
||||||
|
tabBar.currentIndex = 3
|
||||||
|
tabBar.tabClicked(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSwitchToMediaTab: {
|
||||||
|
tabBar.currentIndex = 1
|
||||||
|
tabBar.tabClicked(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaPlayerTab {
|
||||||
|
id: mediaTab
|
||||||
|
}
|
||||||
|
|
||||||
|
WallpaperTab {
|
||||||
|
id: wallpaperTab
|
||||||
|
active: root.currentTabIndex === 2
|
||||||
|
tabBarItem: tabBar
|
||||||
|
keyForwardTarget: mainContainer
|
||||||
|
targetScreen: root.triggerScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
WeatherTab {
|
||||||
|
id: weatherTab
|
||||||
|
visible: SettingsData.weatherEnabled && root.currentTabIndex === 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,759 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
|
property string lastValidTitle: ""
|
||||||
|
property string lastValidArtist: ""
|
||||||
|
property string lastValidAlbum: ""
|
||||||
|
property string lastValidArtUrl: ""
|
||||||
|
property real currentPosition: activePlayer && activePlayer.positionSupported ? activePlayer.position : 0
|
||||||
|
property real displayPosition: currentPosition
|
||||||
|
property var defaultSink: AudioService.sink
|
||||||
|
|
||||||
|
readonly property real ratio: {
|
||||||
|
if (!activePlayer || activePlayer.length <= 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const calculatedRatio = displayPosition / activePlayer.length
|
||||||
|
return Math.max(0, Math.min(1, calculatedRatio))
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: 700
|
||||||
|
implicitHeight: 410
|
||||||
|
|
||||||
|
onActivePlayerChanged: {
|
||||||
|
if (activePlayer && activePlayer.positionSupported) {
|
||||||
|
currentPosition = Qt.binding(() => activePlayer?.position || 0)
|
||||||
|
} else {
|
||||||
|
currentPosition = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: positionTimer
|
||||||
|
interval: 300
|
||||||
|
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressSliderArea.isSeeking
|
||||||
|
repeat: true
|
||||||
|
onTriggered: activePlayer && activePlayer.positionSupported && activePlayer.positionChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: cleanupTimer
|
||||||
|
interval: 2000
|
||||||
|
running: !activePlayer
|
||||||
|
onTriggered: {
|
||||||
|
lastValidTitle = ""
|
||||||
|
lastValidArtist = ""
|
||||||
|
lastValidAlbum = ""
|
||||||
|
lastValidArtUrl = ""
|
||||||
|
currentPosition = 0
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: (!activePlayer && !lastValidTitle) || (activePlayer && activePlayer.trackTitle === "" && lastValidTitle === "")
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "music_note"
|
||||||
|
size: Theme.iconSize * 3
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "No Active Players"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: (activePlayer && activePlayer.trackTitle !== "") || lastValidTitle !== ""
|
||||||
|
|
||||||
|
// Left Column: Album Art and Controls (60%)
|
||||||
|
Column {
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: parent.width * 0.6 - Theme.spacingM
|
||||||
|
height: parent.height
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
// Album Art Section
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height * 0.55
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Math.min(parent.width * 0.8, parent.height * 0.9)
|
||||||
|
height: width
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||||
|
sourceComponent: Component {
|
||||||
|
Ref {
|
||||||
|
service: CavaService
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Shape {
|
||||||
|
id: morphingBlob
|
||||||
|
width: parent.width * 1.1
|
||||||
|
height: parent.height * 1.1
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||||
|
asynchronous: false
|
||||||
|
antialiasing: true
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
z: 0
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
layer.samples: 4
|
||||||
|
|
||||||
|
readonly property real centerX: width / 2
|
||||||
|
readonly property real centerY: height / 2
|
||||||
|
readonly property real baseRadius: Math.min(width, height) * 0.35
|
||||||
|
readonly property int segments: 24
|
||||||
|
|
||||||
|
property var audioLevels: {
|
||||||
|
if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
|
||||||
|
return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5]
|
||||||
|
}
|
||||||
|
return CavaService.values
|
||||||
|
}
|
||||||
|
|
||||||
|
property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5]
|
||||||
|
property var cubics: []
|
||||||
|
|
||||||
|
onAudioLevelsChanged: updatePath()
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
running: morphingBlob.visible
|
||||||
|
interval: 16
|
||||||
|
repeat: true
|
||||||
|
onTriggered: morphingBlob.updatePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: cubicSegment
|
||||||
|
PathCubic {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
shapePath.pathElements.push(Qt.createQmlObject(
|
||||||
|
'import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath
|
||||||
|
))
|
||||||
|
|
||||||
|
for (let i = 0; i < segments; i++) {
|
||||||
|
const seg = cubicSegment.createObject(shapePath)
|
||||||
|
shapePath.pathElements.push(seg)
|
||||||
|
cubics.push(seg)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
function expSmooth(prev, next, alpha) {
|
||||||
|
return prev + alpha * (next - prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePath() {
|
||||||
|
if (cubics.length === 0) return
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(smoothedLevels.length, audioLevels.length); i++) {
|
||||||
|
smoothedLevels[i] = expSmooth(smoothedLevels[i], audioLevels[i], 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = []
|
||||||
|
for (let i = 0; i < segments; i++) {
|
||||||
|
const angle = (i / segments) * 2 * Math.PI
|
||||||
|
const audioIndex = i % Math.min(smoothedLevels.length, 6)
|
||||||
|
const audioLevel = Math.max(0.1, Math.min(1.5, (smoothedLevels[audioIndex] || 0) / 50))
|
||||||
|
|
||||||
|
const radius = baseRadius * (1.0 + audioLevel * 0.3)
|
||||||
|
const x = centerX + Math.cos(angle) * radius
|
||||||
|
const y = centerY + Math.sin(angle) * radius
|
||||||
|
points.push({x: x, y: y})
|
||||||
|
}
|
||||||
|
|
||||||
|
const startMove = shapePath.pathElements[0]
|
||||||
|
startMove.x = points[0].x
|
||||||
|
startMove.y = points[0].y
|
||||||
|
|
||||||
|
const tension = 0.5
|
||||||
|
for (let i = 0; i < segments; i++) {
|
||||||
|
const p0 = points[(i - 1 + segments) % segments]
|
||||||
|
const p1 = points[i]
|
||||||
|
const p2 = points[(i + 1) % segments]
|
||||||
|
const p3 = points[(i + 2) % segments]
|
||||||
|
|
||||||
|
const c1x = p1.x + (p2.x - p0.x) * tension / 3
|
||||||
|
const c1y = p1.y + (p2.y - p0.y) * tension / 3
|
||||||
|
const c2x = p2.x - (p3.x - p1.x) * tension / 3
|
||||||
|
const c2y = p2.y - (p3.y - p1.y) * tension / 3
|
||||||
|
|
||||||
|
const seg = cubics[i]
|
||||||
|
seg.control1X = c1x
|
||||||
|
seg.control1Y = c1y
|
||||||
|
seg.control2X = c2x
|
||||||
|
seg.control2Y = c2y
|
||||||
|
seg.x = p2.x
|
||||||
|
seg.y = p2.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
id: shapePath
|
||||||
|
fillColor: Theme.primary
|
||||||
|
strokeColor: "transparent"
|
||||||
|
strokeWidth: 0
|
||||||
|
joinStyle: ShapePath.RoundJoin
|
||||||
|
fillRule: ShapePath.WindingFill
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width * 0.75
|
||||||
|
height: width
|
||||||
|
radius: width / 2
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
border.color: Theme.surfaceContainer
|
||||||
|
border.width: 1
|
||||||
|
anchors.centerIn: parent
|
||||||
|
z: 1
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: albumArt
|
||||||
|
source: (activePlayer && activePlayer.trackArtUrl) || lastValidArtUrl || ""
|
||||||
|
onSourceChanged: {
|
||||||
|
if (activePlayer && activePlayer.trackArtUrl && albumArt.status !== Image.Error) {
|
||||||
|
lastValidArtUrl = activePlayer.trackArtUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
cache: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: false
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status === Image.Error) {
|
||||||
|
console.warn("Failed to load album art:", source)
|
||||||
|
source = ""
|
||||||
|
if (activePlayer && activePlayer.trackArtUrl === source) {
|
||||||
|
lastValidArtUrl = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiEffect {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
source: albumArt
|
||||||
|
maskEnabled: true
|
||||||
|
maskSource: circularMask
|
||||||
|
visible: albumArt.status === Image.Ready
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: circularMask
|
||||||
|
width: parent.width - 4
|
||||||
|
height: parent.height - 4
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: "black"
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "album"
|
||||||
|
size: parent.width * 0.3
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: albumArt.status !== Image.Ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Song Info and Controls Section
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height * 0.45
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
// Song Info
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: (activePlayer && activePlayer.trackTitle) || lastValidTitle || "Unknown Track"
|
||||||
|
onTextChanged: {
|
||||||
|
if (activePlayer && activePlayer.trackTitle) {
|
||||||
|
lastValidTitle = activePlayer.trackTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: (activePlayer && activePlayer.trackArtist) || lastValidArtist || "Unknown Artist"
|
||||||
|
onTextChanged: {
|
||||||
|
if (activePlayer && activePlayer.trackArtist) {
|
||||||
|
lastValidArtist = activePlayer.trackArtist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: (activePlayer && activePlayer.trackAlbum) || lastValidAlbum || ""
|
||||||
|
onTextChanged: {
|
||||||
|
if (activePlayer && activePlayer.trackAlbum) {
|
||||||
|
lastValidAlbum = activePlayer.trackAlbum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
maximumLineCount: 1
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress Bar
|
||||||
|
Item {
|
||||||
|
id: progressSlider
|
||||||
|
width: parent.width
|
||||||
|
height: 20
|
||||||
|
visible: activePlayer?.length > 0
|
||||||
|
|
||||||
|
property real value: ratio
|
||||||
|
property real lineWidth: 2.5
|
||||||
|
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
|
||||||
|
property color fillColor: Theme.primary
|
||||||
|
property color playheadColor: Theme.primary
|
||||||
|
readonly property real midY: height / 2
|
||||||
|
|
||||||
|
// Background track
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: progressSlider.lineWidth
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: progressSlider.trackColor
|
||||||
|
radius: height / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filled portion
|
||||||
|
Rectangle {
|
||||||
|
width: Math.max(0, Math.min(parent.width, parent.width * progressSlider.value))
|
||||||
|
height: progressSlider.lineWidth
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: progressSlider.fillColor
|
||||||
|
radius: height / 2
|
||||||
|
Behavior on width { NumberAnimation { duration: 80 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playhead
|
||||||
|
Rectangle {
|
||||||
|
id: playhead
|
||||||
|
width: 2.5
|
||||||
|
height: Math.max(progressSlider.lineWidth + 8, 12)
|
||||||
|
radius: width / 2
|
||||||
|
color: progressSlider.playheadColor
|
||||||
|
x: Math.max(0, Math.min(progressSlider.width, progressSlider.width * progressSlider.value)) - width / 2
|
||||||
|
y: progressSlider.midY - height / 2
|
||||||
|
z: 3
|
||||||
|
Behavior on x { NumberAnimation { duration: 80 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: progressSliderArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false
|
||||||
|
|
||||||
|
property bool isSeeking: false
|
||||||
|
property real pendingSeekPosition: -1
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: seekDebounceTimer
|
||||||
|
interval: 150
|
||||||
|
onTriggered: {
|
||||||
|
if (progressSliderArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
|
||||||
|
const clamped = Math.min(progressSliderArea.pendingSeekPosition, activePlayer.length * 0.99)
|
||||||
|
activePlayer.position = clamped
|
||||||
|
progressSliderArea.pendingSeekPosition = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressed: (mouse) => {
|
||||||
|
isSeeking = true
|
||||||
|
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
|
||||||
|
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
|
||||||
|
pendingSeekPosition = r * activePlayer.length
|
||||||
|
displayPosition = pendingSeekPosition
|
||||||
|
seekDebounceTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: {
|
||||||
|
isSeeking = false
|
||||||
|
seekDebounceTimer.stop()
|
||||||
|
if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
|
||||||
|
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
|
||||||
|
activePlayer.position = clamped
|
||||||
|
pendingSeekPosition = -1
|
||||||
|
}
|
||||||
|
displayPosition = Qt.binding(() => currentPosition)
|
||||||
|
}
|
||||||
|
onPositionChanged: (mouse) => {
|
||||||
|
if (pressed && isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) {
|
||||||
|
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
|
||||||
|
pendingSeekPosition = r * activePlayer.length
|
||||||
|
displayPosition = pendingSeekPosition
|
||||||
|
seekDebounceTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
|
||||||
|
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
|
||||||
|
activePlayer.position = r * activePlayer.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media Controls
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
height: 64
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "skip_previous"
|
||||||
|
size: 18
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: prevBtnArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (!activePlayer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activePlayer.position > 8 && activePlayer.canSeek) {
|
||||||
|
activePlayer.position = 0
|
||||||
|
} else {
|
||||||
|
activePlayer.previous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 44
|
||||||
|
height: 44
|
||||||
|
radius: 22
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||||
|
size: 24
|
||||||
|
color: Theme.background
|
||||||
|
weight: 500
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: activePlayer && activePlayer.togglePlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
shadowEnabled: true
|
||||||
|
shadowHorizontalOffset: 0
|
||||||
|
shadowVerticalOffset: 6
|
||||||
|
shadowBlur: 1.0
|
||||||
|
shadowColor: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
shadowOpacity: 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "skip_next"
|
||||||
|
size: 18
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: nextBtnArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: activePlayer && activePlayer.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right Column: Audio Controls (40%)
|
||||||
|
Column {
|
||||||
|
x: parent.width * 0.6 + Theme.spacingM
|
||||||
|
y: 0
|
||||||
|
width: parent.width * 0.4 - Theme.spacingM
|
||||||
|
height: parent.height
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
// Volume Control
|
||||||
|
Row {
|
||||||
|
x: -Theme.spacingS
|
||||||
|
width: parent.width + Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||||
|
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: iconArea
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: defaultSink !== null
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.muted = !defaultSink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
if (!defaultSink) return "volume_off"
|
||||||
|
|
||||||
|
let volume = defaultSink.audio.volume
|
||||||
|
let muted = defaultSink.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingXS
|
||||||
|
enabled: defaultSink !== null
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
value: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.volume = newValue / 100.0
|
||||||
|
if (newValue > 0 && defaultSink.audio.muted) {
|
||||||
|
defaultSink.audio.muted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio Devices
|
||||||
|
DankFlickable {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - y
|
||||||
|
contentHeight: deviceColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: deviceColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Pipewire.nodes.values.filter(node => {
|
||||||
|
return node.audio && node.isSink && !node.isStream
|
||||||
|
})
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 42
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||||
|
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: modelData === AudioService.sink ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
if (modelData.name.includes("bluez"))
|
||||||
|
return "headset"
|
||||||
|
else if (modelData.name.includes("hdmi"))
|
||||||
|
return "tv"
|
||||||
|
else if (modelData.name.includes("usb"))
|
||||||
|
return "headset"
|
||||||
|
else
|
||||||
|
return "speaker"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingS * 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: AudioService.displayName(modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData === AudioService.sink ? "Active" : "Available"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData) {
|
||||||
|
Pipewire.preferredDefaultAudioSink = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: progressMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: false
|
||||||
|
visible: false
|
||||||
|
property bool isSeeking: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,447 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool showEventDetails: false
|
||||||
|
property date selectedDate: systemClock.date
|
||||||
|
property var selectedDateEvents: []
|
||||||
|
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
|
||||||
|
|
||||||
|
signal closeDash()
|
||||||
|
|
||||||
|
function weekStartJs() {
|
||||||
|
return Qt.locale().firstDayOfWeek % 7
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOfWeek(dateObj) {
|
||||||
|
const d = new Date(dateObj)
|
||||||
|
const jsDow = d.getDay()
|
||||||
|
const diff = (jsDow - weekStartJs() + 7) % 7
|
||||||
|
d.setDate(d.getDate() - diff)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
function endOfWeek(dateObj) {
|
||||||
|
const d = new Date(dateObj)
|
||||||
|
const jsDow = d.getDay()
|
||||||
|
const add = (weekStartJs() + 6 - jsDow + 7) % 7
|
||||||
|
d.setDate(d.getDate() + add)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedDateEvents() {
|
||||||
|
if (CalendarService && CalendarService.khalAvailable) {
|
||||||
|
const events = CalendarService.getEventsForDate(selectedDate)
|
||||||
|
selectedDateEvents = events
|
||||||
|
} else {
|
||||||
|
selectedDateEvents = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEventsForMonth() {
|
||||||
|
if (!CalendarService || !CalendarService.khalAvailable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstOfMonth = new Date(calendarGrid.displayDate.getFullYear(),
|
||||||
|
calendarGrid.displayDate.getMonth(), 1)
|
||||||
|
const lastOfMonth = new Date(calendarGrid.displayDate.getFullYear(),
|
||||||
|
calendarGrid.displayDate.getMonth() + 1, 0)
|
||||||
|
|
||||||
|
const startDate = startOfWeek(firstOfMonth)
|
||||||
|
startDate.setDate(startDate.getDate() - 7)
|
||||||
|
|
||||||
|
const endDate = endOfWeek(lastOfMonth)
|
||||||
|
endDate.setDate(endDate.getDate() + 7)
|
||||||
|
|
||||||
|
CalendarService.loadEvents(startDate, endDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectedDateChanged: updateSelectedDateEvents()
|
||||||
|
Component.onCompleted: {
|
||||||
|
loadEventsForMonth()
|
||||||
|
updateSelectedDateEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onEventsByDateChanged() {
|
||||||
|
updateSelectedDateEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKhalAvailableChanged() {
|
||||||
|
if (CalendarService && CalendarService.khalAvailable) {
|
||||||
|
loadEventsForMonth()
|
||||||
|
}
|
||||||
|
updateSelectedDateEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
target: CalendarService
|
||||||
|
enabled: CalendarService !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
visible: showEventDetails
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: backButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "arrow_back"
|
||||||
|
size: 14
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: backButtonArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.showEventDetails = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: 32 + Theme.spacingS * 2
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: {
|
||||||
|
const dateStr = Qt.formatDate(selectedDate, "MMM d")
|
||||||
|
if (selectedDateEvents && selectedDateEvents.length > 0) {
|
||||||
|
const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 event") : selectedDateEvents.length + " " + I18n.tr("events")
|
||||||
|
return dateStr + " • " + eventCount
|
||||||
|
}
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 28
|
||||||
|
visible: !showEventDetails
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "chevron_left"
|
||||||
|
size: 14
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: prevMonthArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
let newDate = new Date(calendarGrid.displayDate)
|
||||||
|
newDate.setMonth(newDate.getMonth() - 1)
|
||||||
|
calendarGrid.displayDate = newDate
|
||||||
|
loadEventsForMonth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width - 56
|
||||||
|
height: 28
|
||||||
|
text: calendarGrid.displayDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "chevron_right"
|
||||||
|
size: 14
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: nextMonthArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
let newDate = new Date(calendarGrid.displayDate)
|
||||||
|
newDate.setMonth(newDate.getMonth() + 1)
|
||||||
|
calendarGrid.displayDate = newDate
|
||||||
|
loadEventsForMonth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 18
|
||||||
|
visible: !showEventDetails
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: {
|
||||||
|
const days = []
|
||||||
|
const loc = Qt.locale()
|
||||||
|
const qtFirst = loc.firstDayOfWeek
|
||||||
|
for (let i = 0; i < 7; ++i) {
|
||||||
|
const qtDay = ((qtFirst - 1 + i) % 7) + 1
|
||||||
|
days.push(loc.dayName(qtDay, Locale.ShortFormat))
|
||||||
|
}
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width / 7
|
||||||
|
height: 18
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
id: calendarGrid
|
||||||
|
visible: !showEventDetails
|
||||||
|
|
||||||
|
property date displayDate: systemClock.date
|
||||||
|
property date selectedDate: systemClock.date
|
||||||
|
|
||||||
|
readonly property date firstDay: {
|
||||||
|
const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
|
||||||
|
return startOfWeek(firstOfMonth)
|
||||||
|
}
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 28 - 18 - Theme.spacingS * 2
|
||||||
|
columns: 7
|
||||||
|
rows: 6
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: 42
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
readonly property date dayDate: {
|
||||||
|
const date = new Date(parent.firstDay)
|
||||||
|
date.setDate(date.getDate() + index)
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth()
|
||||||
|
readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
|
||||||
|
readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString()
|
||||||
|
|
||||||
|
width: parent.width / 7
|
||||||
|
height: parent.height / 6
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Math.min(parent.width - 4, parent.height - 4, 32)
|
||||||
|
height: width
|
||||||
|
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: width / 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: dayDate.getDate()
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||||
|
font.weight: isToday ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.bottomMargin: 4
|
||||||
|
width: 12
|
||||||
|
height: 2
|
||||||
|
radius: 1
|
||||||
|
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
|
||||||
|
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
|
||||||
|
opacity: isToday ? 0.9 : 0.7
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: dayArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
|
||||||
|
root.selectedDate = dayDate
|
||||||
|
root.showEventDetails = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DankListView {
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
model: selectedDateEvents
|
||||||
|
visible: showEventDetails
|
||||||
|
clip: true
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: parent ? parent.width : 0
|
||||||
|
height: eventContent.implicitHeight + Theme.spacingS
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (modelData.url && eventMouseArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||||
|
} else if (eventMouseArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06)
|
||||||
|
}
|
||||||
|
return Theme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
border.color: {
|
||||||
|
if (modelData.url && eventMouseArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||||
|
} else if (eventMouseArea.containsMouse) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
|
||||||
|
}
|
||||||
|
return "transparent"
|
||||||
|
}
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 3
|
||||||
|
height: parent.height - 6
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 3
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: 2
|
||||||
|
color: Theme.primary
|
||||||
|
opacity: 0.8
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: eventContent
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingS + 6
|
||||||
|
anchors.rightMargin: Theme.spacingXS
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: modelData.title
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: {
|
||||||
|
if (!modelData || modelData.allDay) {
|
||||||
|
return I18n.tr("All day")
|
||||||
|
} else if (modelData.start && modelData.end) {
|
||||||
|
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
|
||||||
|
const startTime = Qt.formatTime(modelData.start, timeFormat)
|
||||||
|
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) {
|
||||||
|
return startTime + " – " + Qt.formatTime(modelData.end, timeFormat)
|
||||||
|
}
|
||||||
|
return startTime
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
font.weight: Font.Normal
|
||||||
|
visible: text !== ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: eventMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
enabled: modelData.url !== ""
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.url && modelData.url !== "") {
|
||||||
|
if (Qt.openUrlExternally(modelData.url) === false) {
|
||||||
|
console.warn("Failed to open URL: " + modelData.url)
|
||||||
|
} else {
|
||||||
|
root.closeDash()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemClock {
|
||||||
|
id: systemClock
|
||||||
|
precision: SystemClock.Hours
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: card
|
||||||
|
|
||||||
|
property int pad: Theme.spacingM
|
||||||
|
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
default property alias content: contentItem.data
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: contentItem
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: card.pad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Card {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: -8
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 0
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (SettingsData.use24HourClock) {
|
||||||
|
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(0)
|
||||||
|
} else {
|
||||||
|
const hours = systemClock?.date?.getHours()
|
||||||
|
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
|
||||||
|
return String(display).padStart(2, '0').charAt(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: 48
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: 28
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (SettingsData.use24HourClock) {
|
||||||
|
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(1)
|
||||||
|
} else {
|
||||||
|
const hours = systemClock?.date?.getHours()
|
||||||
|
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
|
||||||
|
return String(display).padStart(2, '0').charAt(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: 48
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: 28
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 0
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0)
|
||||||
|
font.pixelSize: 48
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: 28
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1)
|
||||||
|
font.pixelSize: 48
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: 28
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
|
||||||
|
return systemClock?.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat)
|
||||||
|
}
|
||||||
|
return systemClock?.date?.toLocaleDateString(Qt.locale(), "MMM d")
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemClock {
|
||||||
|
id: systemClock
|
||||||
|
precision: SystemClock.Seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,481 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Card {
|
||||||
|
id: root
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
|
property real currentPosition: activePlayer?.positionSupported ? activePlayer.position : 0
|
||||||
|
property real displayPosition: currentPosition
|
||||||
|
|
||||||
|
readonly property real ratio: {
|
||||||
|
if (!activePlayer || activePlayer.length <= 0) return 0
|
||||||
|
const calculatedRatio = displayPosition / activePlayer.length
|
||||||
|
return Math.max(0, Math.min(1, calculatedRatio))
|
||||||
|
}
|
||||||
|
|
||||||
|
onActivePlayerChanged: {
|
||||||
|
if (activePlayer?.positionSupported) {
|
||||||
|
currentPosition = Qt.binding(() => activePlayer?.position || 0)
|
||||||
|
} else {
|
||||||
|
currentPosition = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 300
|
||||||
|
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking
|
||||||
|
repeat: true
|
||||||
|
onTriggered: activePlayer?.positionSupported && activePlayer.positionChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: !activePlayer
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "music_note"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "No Media"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingXS * 2
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
visible: activePlayer
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 110
|
||||||
|
height: 80
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||||
|
sourceComponent: Component {
|
||||||
|
Ref {
|
||||||
|
service: CavaService
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Shape {
|
||||||
|
id: morphingBlob
|
||||||
|
width: 120
|
||||||
|
height: 120
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||||
|
asynchronous: false
|
||||||
|
antialiasing: true
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
layer.samples: 4
|
||||||
|
|
||||||
|
|
||||||
|
readonly property real centerX: width / 2
|
||||||
|
readonly property real centerY: height / 2
|
||||||
|
readonly property real baseRadius: 40
|
||||||
|
readonly property int segments: 24
|
||||||
|
|
||||||
|
property var audioLevels: {
|
||||||
|
if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
|
||||||
|
return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5]
|
||||||
|
}
|
||||||
|
return CavaService.values
|
||||||
|
}
|
||||||
|
|
||||||
|
property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5]
|
||||||
|
property var cubics: []
|
||||||
|
|
||||||
|
|
||||||
|
onAudioLevelsChanged: updatePath()
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
running: morphingBlob.visible
|
||||||
|
interval: 16
|
||||||
|
repeat: true
|
||||||
|
onTriggered: morphingBlob.updatePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: cubicSegment
|
||||||
|
PathCubic {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
shapePath.pathElements.push(Qt.createQmlObject(
|
||||||
|
'import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath
|
||||||
|
))
|
||||||
|
|
||||||
|
for (let i = 0; i < segments; i++) {
|
||||||
|
const seg = cubicSegment.createObject(shapePath)
|
||||||
|
shapePath.pathElements.push(seg)
|
||||||
|
cubics.push(seg)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
function expSmooth(prev, next, alpha) {
|
||||||
|
return prev + alpha * (next - prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePath() {
|
||||||
|
if (cubics.length === 0) return
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(smoothedLevels.length, audioLevels.length); i++) {
|
||||||
|
smoothedLevels[i] = expSmooth(smoothedLevels[i], audioLevels[i], 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = []
|
||||||
|
for (let i = 0; i < segments; i++) {
|
||||||
|
const angle = (i / segments) * 2 * Math.PI
|
||||||
|
const audioIndex = i % Math.min(smoothedLevels.length, 6)
|
||||||
|
const audioLevel = Math.max(0.1, Math.min(1.5, (smoothedLevels[audioIndex] || 0) / 50))
|
||||||
|
|
||||||
|
const radius = baseRadius * (1.0 + audioLevel * 0.3)
|
||||||
|
const x = centerX + Math.cos(angle) * radius
|
||||||
|
const y = centerY + Math.sin(angle) * radius
|
||||||
|
points.push({x: x, y: y})
|
||||||
|
}
|
||||||
|
|
||||||
|
const startMove = shapePath.pathElements[0]
|
||||||
|
startMove.x = points[0].x
|
||||||
|
startMove.y = points[0].y
|
||||||
|
|
||||||
|
const tension = 0.5
|
||||||
|
for (let i = 0; i < segments; i++) {
|
||||||
|
const p0 = points[(i - 1 + segments) % segments]
|
||||||
|
const p1 = points[i]
|
||||||
|
const p2 = points[(i + 1) % segments]
|
||||||
|
const p3 = points[(i + 2) % segments]
|
||||||
|
|
||||||
|
const c1x = p1.x + (p2.x - p0.x) * tension / 3
|
||||||
|
const c1y = p1.y + (p2.y - p0.y) * tension / 3
|
||||||
|
const c2x = p2.x - (p3.x - p1.x) * tension / 3
|
||||||
|
const c2y = p2.y - (p3.y - p1.y) * tension / 3
|
||||||
|
|
||||||
|
const seg = cubics[i]
|
||||||
|
seg.control1X = c1x
|
||||||
|
seg.control1Y = c1y
|
||||||
|
seg.control2X = c2x
|
||||||
|
seg.control2Y = c2y
|
||||||
|
seg.x = p2.x
|
||||||
|
seg.y = p2.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
id: shapePath
|
||||||
|
fillColor: Theme.primary
|
||||||
|
strokeColor: "transparent"
|
||||||
|
strokeWidth: 0
|
||||||
|
joinStyle: ShapePath.RoundJoin
|
||||||
|
fillRule: ShapePath.WindingFill
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 72
|
||||||
|
height: 72
|
||||||
|
radius: 36
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
border.color: Theme.surfaceContainer
|
||||||
|
border.width: 1
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
z: 1
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: albumArt
|
||||||
|
source: activePlayer?.trackArtUrl || ""
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
cache: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiEffect {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
source: albumArt
|
||||||
|
maskEnabled: true
|
||||||
|
maskSource: circularMask
|
||||||
|
visible: albumArt.status === Image.Ready
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: circularMask
|
||||||
|
width: 68
|
||||||
|
height: 68
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: "black"
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "album"
|
||||||
|
size: 20
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: albumArt.status !== Image.Ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
topPadding: Theme.spacingL
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: activePlayer?.trackTitle || "Unknown"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: activePlayer?.trackArtist || "Unknown Artist"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: progressSlider
|
||||||
|
width: parent.width
|
||||||
|
height: 20
|
||||||
|
visible: activePlayer?.length > 0
|
||||||
|
|
||||||
|
property real value: ratio
|
||||||
|
property real lineWidth: 2.5
|
||||||
|
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
|
||||||
|
property color fillColor: Theme.primary
|
||||||
|
property color playheadColor: Theme.primary
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: progressSlider.lineWidth
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: progressSlider.trackColor
|
||||||
|
radius: height / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Math.max(0, Math.min(parent.width, parent.width * progressSlider.value))
|
||||||
|
height: progressSlider.lineWidth
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: progressSlider.fillColor
|
||||||
|
radius: height / 2
|
||||||
|
Behavior on width { NumberAnimation { duration: 80 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: playhead
|
||||||
|
width: 2.5
|
||||||
|
height: Math.max(progressSlider.lineWidth + 8, 12)
|
||||||
|
radius: width / 2
|
||||||
|
color: progressSlider.playheadColor
|
||||||
|
x: Math.max(0, Math.min(progressSlider.width, progressSlider.width * progressSlider.value)) - width / 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
z: 3
|
||||||
|
Behavior on x { NumberAnimation { duration: 80 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: progressMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false
|
||||||
|
|
||||||
|
property bool isSeeking: false
|
||||||
|
property real pendingSeekPosition: -1
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: seekDebounceTimer
|
||||||
|
interval: 150
|
||||||
|
onTriggered: {
|
||||||
|
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
|
||||||
|
const clamped = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
|
||||||
|
activePlayer.position = clamped
|
||||||
|
progressMouseArea.pendingSeekPosition = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressed: (mouse) => {
|
||||||
|
isSeeking = true
|
||||||
|
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
|
||||||
|
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
|
||||||
|
pendingSeekPosition = r * activePlayer.length
|
||||||
|
displayPosition = pendingSeekPosition
|
||||||
|
seekDebounceTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: {
|
||||||
|
isSeeking = false
|
||||||
|
seekDebounceTimer.stop()
|
||||||
|
if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
|
||||||
|
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
|
||||||
|
activePlayer.position = clamped
|
||||||
|
pendingSeekPosition = -1
|
||||||
|
}
|
||||||
|
displayPosition = Qt.binding(() => currentPosition)
|
||||||
|
}
|
||||||
|
onPositionChanged: (mouse) => {
|
||||||
|
if (pressed && isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) {
|
||||||
|
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
|
||||||
|
pendingSeekPosition = r * activePlayer.length
|
||||||
|
displayPosition = pendingSeekPosition
|
||||||
|
seekDebounceTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
|
||||||
|
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
|
||||||
|
activePlayer.position = r * activePlayer.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
anchors.verticalCenter: playPauseButton.verticalCenter
|
||||||
|
color: prevArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "skip_previous"
|
||||||
|
size: 14
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: prevArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (!activePlayer) return
|
||||||
|
if (activePlayer.position > 8 && activePlayer.canSeek) {
|
||||||
|
activePlayer.position = 0
|
||||||
|
} else {
|
||||||
|
activePlayer.previous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: playPauseButton
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: Theme.primary
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||||
|
size: 16
|
||||||
|
color: Theme.background
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: activePlayer?.togglePlaying()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
anchors.verticalCenter: playPauseButton.verticalCenter
|
||||||
|
color: nextArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "skip_next"
|
||||||
|
size: 14
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: nextArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: activePlayer?.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 123
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.clicked()
|
||||||
|
visible: activePlayer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Card {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
DgopService.addRef(["cpu", "memory", "system"])
|
||||||
|
}
|
||||||
|
Component.onDestruction: {
|
||||||
|
DgopService.removeRef(["cpu", "memory", "system"])
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
// CPU Bar
|
||||||
|
Column {
|
||||||
|
width: (parent.width - 2 * Theme.spacingM) / 3
|
||||||
|
height: parent.height
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 8
|
||||||
|
height: parent.height - Theme.iconSizeSmall - Theme.spacingS
|
||||||
|
radius: 4
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height * Math.min((DgopService.cpuUsage || 6) / 100, 1)
|
||||||
|
radius: parent.radius
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: {
|
||||||
|
if (DgopService.cpuUsage > 80) return Theme.error
|
||||||
|
if (DgopService.cpuUsage > 60) return Theme.warning
|
||||||
|
return Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: Theme.iconSizeSmall
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "memory"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
color: {
|
||||||
|
if (DgopService.cpuUsage > 80) return Theme.error
|
||||||
|
if (DgopService.cpuUsage > 60) return Theme.warning
|
||||||
|
return Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temperature Bar
|
||||||
|
Column {
|
||||||
|
width: (parent.width - 2 * Theme.spacingM) / 3
|
||||||
|
height: parent.height
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 8
|
||||||
|
height: parent.height - Theme.iconSizeSmall - Theme.spacingS
|
||||||
|
radius: 4
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height * Math.min(Math.max((DgopService.cpuTemperature || 40) / 100, 0), 1)
|
||||||
|
radius: parent.radius
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: {
|
||||||
|
if (DgopService.cpuTemperature > 85) return Theme.error
|
||||||
|
if (DgopService.cpuTemperature > 69) return Theme.warning
|
||||||
|
return Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: Theme.iconSizeSmall
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "device_thermostat"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
color: {
|
||||||
|
if (DgopService.cpuTemperature > 85) return Theme.error
|
||||||
|
if (DgopService.cpuTemperature > 69) return Theme.warning
|
||||||
|
return Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAM Bar
|
||||||
|
Column {
|
||||||
|
width: (parent.width - 2 * Theme.spacingM) / 3
|
||||||
|
height: parent.height
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 8
|
||||||
|
height: parent.height - Theme.iconSizeSmall - Theme.spacingS
|
||||||
|
radius: 4
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height * Math.min((DgopService.memoryUsage || 42) / 100, 1)
|
||||||
|
radius: parent.radius
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: {
|
||||||
|
if (DgopService.memoryUsage > 90) return Theme.error
|
||||||
|
if (DgopService.memoryUsage > 75) return Theme.warning
|
||||||
|
return Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: Theme.iconSizeSmall
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "developer_board"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
color: {
|
||||||
|
if (DgopService.memoryUsage > 90) return Theme.error
|
||||||
|
if (DgopService.memoryUsage > 75) return Theme.warning
|
||||||
|
return Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Card {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: avatarContainer
|
||||||
|
|
||||||
|
property bool hasImage: profileImageLoader.status === Image.Ready
|
||||||
|
|
||||||
|
width: 77
|
||||||
|
height: 77
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 36
|
||||||
|
color: Theme.primary
|
||||||
|
visible: !avatarContainer.hasImage
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: UserInfoService.username.length > 0 ? UserInfoService.username.charAt(0).toUpperCase() : "b"
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge + 4
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.background
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: profileImageLoader
|
||||||
|
|
||||||
|
source: {
|
||||||
|
if (PortalService.profileImage === "")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if (PortalService.profileImage.startsWith("/"))
|
||||||
|
return "file://" + PortalService.profileImage
|
||||||
|
|
||||||
|
return PortalService.profileImage
|
||||||
|
}
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
mipmap: true
|
||||||
|
cache: true
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiEffect {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
source: profileImageLoader
|
||||||
|
maskEnabled: true
|
||||||
|
maskSource: circularMask
|
||||||
|
visible: avatarContainer.hasImage
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: circularMask
|
||||||
|
width: 77 - 4
|
||||||
|
height: 77 - 4
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: "black"
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "person"
|
||||||
|
size: Theme.iconSize + 8
|
||||||
|
color: Theme.error
|
||||||
|
visible: PortalService.profileImage !== "" && profileImageLoader.status === Image.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: UserInfoService.username || "brandon"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
SystemLogo {
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
colorOverride: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (CompositorService.isNiri) return "on niri"
|
||||||
|
if (CompositorService.isHyprland) return "on Hyprland"
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "schedule"
|
||||||
|
size: 16
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: uptimeText
|
||||||
|
|
||||||
|
property real availableWidth: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS
|
||||||
|
property real longTextWidth: {
|
||||||
|
const testMetrics = Qt.createQmlObject('import QtQuick; TextMetrics { font.pixelSize: ' + Theme.fontSizeSmall + ' }', uptimeText)
|
||||||
|
testMetrics.text = UserInfoService.uptime || "up 1 hour, 23 minutes"
|
||||||
|
const result = testMetrics.width
|
||||||
|
testMetrics.destroy()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
// Just using truncated is always true initially idk
|
||||||
|
property bool shouldUseShort: longTextWidth > availableWidth
|
||||||
|
|
||||||
|
text: shouldUseShort ? UserInfoService.shortUptime : UserInfoService.uptime || "up 1h 23m"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: availableWidth
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Card {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
Component.onCompleted: WeatherService.addRef()
|
||||||
|
Component.onDestruction: WeatherService.removeRef()
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "cloud_off"
|
||||||
|
size: 24
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: WeatherService.weather.loading ? "Loading..." : "No Weather"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Refresh"
|
||||||
|
flat: true
|
||||||
|
visible: !WeatherService.weather.loading
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
onClicked: WeatherService.forceRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
|
||||||
|
size: 48
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp;
|
||||||
|
if (temp === undefined || temp === null || temp === 0) {
|
||||||
|
return "--°" + (SettingsData.useFahrenheit ? "F" : "C");
|
||||||
|
}
|
||||||
|
return temp + "°" + (SettingsData.useFahrenheit ? "F" : "C");
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge + 4
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Light
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: WeatherService.getWeatherCondition(WeatherService.weather.wCode)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.parent.parent.width - 48 - Theme.spacingL * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.clicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.DankDash.Overview
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
implicitWidth: 700
|
||||||
|
implicitHeight: 410
|
||||||
|
|
||||||
|
signal switchToWeatherTab()
|
||||||
|
signal switchToMediaTab()
|
||||||
|
signal closeDash()
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
// Clock - top left (narrower and shorter)
|
||||||
|
ClockCard {
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: parent.width * 0.2 - Theme.spacingM * 2
|
||||||
|
height: 180
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather - top middle-left (narrower)
|
||||||
|
WeatherOverviewCard {
|
||||||
|
x: SettingsData.weatherEnabled ? parent.width * 0.2 - Theme.spacingM : 0
|
||||||
|
y: 0
|
||||||
|
width: SettingsData.weatherEnabled ? parent.width * 0.3 : 0
|
||||||
|
height: 100
|
||||||
|
visible: SettingsData.weatherEnabled
|
||||||
|
|
||||||
|
onClicked: root.switchToWeatherTab()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInfo - top middle-right (extend when weather disabled)
|
||||||
|
UserInfoCard {
|
||||||
|
x: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.2 - Theme.spacingM
|
||||||
|
y: 0
|
||||||
|
width: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.8
|
||||||
|
height: 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemMonitor - middle left (narrow and shorter)
|
||||||
|
SystemMonitorCard {
|
||||||
|
x: 0
|
||||||
|
y: 180 + Theme.spacingM
|
||||||
|
width: parent.width * 0.2 - Theme.spacingM * 2
|
||||||
|
height: 220
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calendar - bottom middle (wider and taller)
|
||||||
|
CalendarOverviewCard {
|
||||||
|
x: parent.width * 0.2 - Theme.spacingM
|
||||||
|
y: 100 + Theme.spacingM
|
||||||
|
width: parent.width * 0.6
|
||||||
|
height: 300
|
||||||
|
|
||||||
|
onCloseDash: root.closeDash()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media - bottom right (narrow and taller)
|
||||||
|
MediaOverviewCard {
|
||||||
|
x: parent.width * 0.8
|
||||||
|
y: 100 + Theme.spacingM
|
||||||
|
width: parent.width * 0.2
|
||||||
|
height: 300
|
||||||
|
|
||||||
|
onClicked: root.switchToMediaTab()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
525
quickshell/.config/quickshell/Modules/DankDash/WallpaperTab.qml
Normal file
525
quickshell/.config/quickshell/Modules/DankDash/WallpaperTab.qml
Normal file
|
|
@ -0,0 +1,525 @@
|
||||||
|
import Qt.labs.folderlistmodel
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.FileBrowser
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
implicitWidth: 700
|
||||||
|
implicitHeight: 410
|
||||||
|
|
||||||
|
property string wallpaperDir: ""
|
||||||
|
property int currentPage: 0
|
||||||
|
property int itemsPerPage: 16
|
||||||
|
property int totalPages: Math.max(1, Math.ceil(wallpaperFolderModel.count / itemsPerPage))
|
||||||
|
property bool active: false
|
||||||
|
property Item focusTarget: wallpaperGrid
|
||||||
|
property Item tabBarItem: null
|
||||||
|
property int gridIndex: 0
|
||||||
|
property Item keyForwardTarget: null
|
||||||
|
property var parentPopout: null
|
||||||
|
property int lastPage: 0
|
||||||
|
property bool enableAnimation: false
|
||||||
|
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||||
|
property string selectedFileName: ""
|
||||||
|
property var targetScreen: null
|
||||||
|
property string targetScreenName: targetScreen ? targetScreen.name : ""
|
||||||
|
|
||||||
|
signal requestTabChange(int newIndex)
|
||||||
|
|
||||||
|
function getCurrentWallpaper() {
|
||||||
|
if (SessionData.perMonitorWallpaper && targetScreenName) {
|
||||||
|
return SessionData.getMonitorWallpaper(targetScreenName);
|
||||||
|
}
|
||||||
|
return SessionData.wallpaperPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentWallpaper(path) {
|
||||||
|
if (SessionData.perMonitorWallpaper && targetScreenName) {
|
||||||
|
SessionData.setMonitorWallpaper(targetScreenName, path);
|
||||||
|
} else {
|
||||||
|
SessionData.setWallpaper(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCurrentPageChanged: {
|
||||||
|
if (currentPage !== lastPage) {
|
||||||
|
enableAnimation = false;
|
||||||
|
lastPage = currentPage;
|
||||||
|
}
|
||||||
|
updateSelectedFileName();
|
||||||
|
}
|
||||||
|
|
||||||
|
onGridIndexChanged: {
|
||||||
|
updateSelectedFileName();
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible && active) {
|
||||||
|
setInitialSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
loadWallpaperDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active && visible) {
|
||||||
|
setInitialSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyEvent(event) {
|
||||||
|
const columns = 4;
|
||||||
|
const currentCol = gridIndex % columns;
|
||||||
|
const visibleCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
if (gridIndex >= 0 && gridIndex < visibleCount) {
|
||||||
|
const absoluteIndex = currentPage * itemsPerPage + gridIndex;
|
||||||
|
if (absoluteIndex < wallpaperFolderModel.count) {
|
||||||
|
const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath");
|
||||||
|
if (filePath) {
|
||||||
|
setCurrentWallpaper(filePath.toString().replace(/^file:\/\//, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Right || event.key === Qt.Key_L) {
|
||||||
|
if (gridIndex + 1 < visibleCount) {
|
||||||
|
gridIndex++;
|
||||||
|
} else if (currentPage < totalPages - 1) {
|
||||||
|
gridIndex = 0;
|
||||||
|
currentPage++;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Left || event.key === Qt.Key_H) {
|
||||||
|
if (gridIndex > 0) {
|
||||||
|
gridIndex--;
|
||||||
|
} else if (currentPage > 0) {
|
||||||
|
currentPage--;
|
||||||
|
const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
|
||||||
|
gridIndex = prevPageCount - 1;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Down || event.key === Qt.Key_J) {
|
||||||
|
if (gridIndex + columns < visibleCount) {
|
||||||
|
gridIndex += columns;
|
||||||
|
} else if (currentPage < totalPages - 1) {
|
||||||
|
gridIndex = currentCol;
|
||||||
|
currentPage++;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Up || event.key === Qt.Key_K) {
|
||||||
|
if (gridIndex >= columns) {
|
||||||
|
gridIndex -= columns;
|
||||||
|
} else if (currentPage > 0) {
|
||||||
|
currentPage--;
|
||||||
|
const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
|
||||||
|
const prevPageRows = Math.ceil(prevPageCount / columns);
|
||||||
|
gridIndex = (prevPageRows - 1) * columns + currentCol;
|
||||||
|
gridIndex = Math.min(gridIndex, prevPageCount - 1);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_PageUp && currentPage > 0) {
|
||||||
|
gridIndex = 0;
|
||||||
|
currentPage--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_PageDown && currentPage < totalPages - 1) {
|
||||||
|
gridIndex = 0;
|
||||||
|
currentPage++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Home && event.modifiers & Qt.ControlModifier) {
|
||||||
|
gridIndex = 0;
|
||||||
|
currentPage = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_End && event.modifiers & Qt.ControlModifier) {
|
||||||
|
currentPage = totalPages - 1;
|
||||||
|
const lastPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
|
||||||
|
gridIndex = Math.max(0, lastPageCount - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInitialSelection() {
|
||||||
|
const currentWallpaper = getCurrentWallpaper();
|
||||||
|
if (!currentWallpaper || wallpaperFolderModel.count === 0) {
|
||||||
|
gridIndex = 0;
|
||||||
|
updateSelectedFileName();
|
||||||
|
Qt.callLater(() => {
|
||||||
|
enableAnimation = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < wallpaperFolderModel.count; i++) {
|
||||||
|
const filePath = wallpaperFolderModel.get(i, "filePath");
|
||||||
|
if (filePath && filePath.toString().replace(/^file:\/\//, '') === currentWallpaper) {
|
||||||
|
const targetPage = Math.floor(i / itemsPerPage);
|
||||||
|
const targetIndex = i % itemsPerPage;
|
||||||
|
currentPage = targetPage;
|
||||||
|
gridIndex = targetIndex;
|
||||||
|
updateSelectedFileName();
|
||||||
|
Qt.callLater(() => {
|
||||||
|
enableAnimation = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gridIndex = 0;
|
||||||
|
updateSelectedFileName();
|
||||||
|
Qt.callLater(() => {
|
||||||
|
enableAnimation = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadWallpaperDirectory() {
|
||||||
|
const currentWallpaper = getCurrentWallpaper();
|
||||||
|
|
||||||
|
if (!currentWallpaper || currentWallpaper.startsWith("#")) {
|
||||||
|
if (CacheData.wallpaperLastPath && CacheData.wallpaperLastPath !== "") {
|
||||||
|
wallpaperDir = CacheData.wallpaperLastPath;
|
||||||
|
} else {
|
||||||
|
wallpaperDir = "";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedFileName() {
|
||||||
|
if (wallpaperFolderModel.count === 0) {
|
||||||
|
selectedFileName = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absoluteIndex = currentPage * itemsPerPage + gridIndex;
|
||||||
|
if (absoluteIndex < wallpaperFolderModel.count) {
|
||||||
|
const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath");
|
||||||
|
if (filePath) {
|
||||||
|
const pathStr = filePath.toString().replace(/^file:\/\//, '');
|
||||||
|
selectedFileName = pathStr.substring(pathStr.lastIndexOf('/') + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedFileName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SessionData
|
||||||
|
function onWallpaperPathChanged() {
|
||||||
|
loadWallpaperDirectory();
|
||||||
|
if (visible && active) {
|
||||||
|
setInitialSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onMonitorWallpapersChanged() {
|
||||||
|
loadWallpaperDirectory();
|
||||||
|
if (visible && active) {
|
||||||
|
setInitialSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onPerMonitorWallpaperChanged() {
|
||||||
|
loadWallpaperDirectory();
|
||||||
|
if (visible && active) {
|
||||||
|
setInitialSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTargetScreenNameChanged: {
|
||||||
|
loadWallpaperDirectory();
|
||||||
|
if (visible && active) {
|
||||||
|
setInitialSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: wallpaperFolderModel
|
||||||
|
function onCountChanged() {
|
||||||
|
if (wallpaperFolderModel.status === FolderListModel.Ready) {
|
||||||
|
if (visible && active) {
|
||||||
|
setInitialSelection();
|
||||||
|
}
|
||||||
|
updateSelectedFileName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onStatusChanged() {
|
||||||
|
if (wallpaperFolderModel.status === FolderListModel.Ready && wallpaperFolderModel.count > 0) {
|
||||||
|
if (visible && active) {
|
||||||
|
setInitialSelection();
|
||||||
|
}
|
||||||
|
updateSelectedFileName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderListModel {
|
||||||
|
id: wallpaperFolderModel
|
||||||
|
|
||||||
|
showDirsFirst: false
|
||||||
|
showDotAndDotDot: false
|
||||||
|
showHidden: false
|
||||||
|
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||||
|
showFiles: true
|
||||||
|
showDirs: false
|
||||||
|
sortField: FolderListModel.Name
|
||||||
|
folder: wallpaperDir ? "file://" + wallpaperDir.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 50
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
id: wallpaperGrid
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingS
|
||||||
|
height: parent.height - Theme.spacingS
|
||||||
|
cellWidth: width / 4
|
||||||
|
cellHeight: height / 4
|
||||||
|
clip: true
|
||||||
|
enabled: root.active
|
||||||
|
interactive: root.active
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
keyNavigationEnabled: false
|
||||||
|
activeFocusOnTab: false
|
||||||
|
highlightFollowsCurrentItem: true
|
||||||
|
highlightMoveDuration: enableAnimation ? Theme.shortDuration : 0
|
||||||
|
focus: false
|
||||||
|
|
||||||
|
highlight: Item {
|
||||||
|
z: 1000
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingXS
|
||||||
|
color: "transparent"
|
||||||
|
border.width: 3
|
||||||
|
border.color: Theme.primary
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model: {
|
||||||
|
const startIndex = currentPage * itemsPerPage;
|
||||||
|
const endIndex = Math.min(startIndex + itemsPerPage, wallpaperFolderModel.count);
|
||||||
|
const items = [];
|
||||||
|
for (var i = startIndex; i < endIndex; i++) {
|
||||||
|
const filePath = wallpaperFolderModel.get(i, "filePath");
|
||||||
|
if (filePath) {
|
||||||
|
items.push(filePath.toString().replace(/^file:\/\//, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
onModelChanged: {
|
||||||
|
const clampedIndex = model.length > 0 ? Math.min(Math.max(0, gridIndex), model.length - 1) : 0;
|
||||||
|
if (gridIndex !== clampedIndex) {
|
||||||
|
gridIndex = clampedIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCountChanged: {
|
||||||
|
if (count > 0) {
|
||||||
|
const clampedIndex = Math.min(gridIndex, count - 1);
|
||||||
|
currentIndex = clampedIndex;
|
||||||
|
positionViewAtIndex(clampedIndex, GridView.Contain);
|
||||||
|
}
|
||||||
|
Qt.callLater(() => {
|
||||||
|
enableAnimation = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onGridIndexChanged() {
|
||||||
|
if (wallpaperGrid.count > 0) {
|
||||||
|
wallpaperGrid.currentIndex = gridIndex;
|
||||||
|
if (!enableAnimation) {
|
||||||
|
wallpaperGrid.positionViewAtIndex(gridIndex, GridView.Contain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
width: wallpaperGrid.cellWidth
|
||||||
|
height: wallpaperGrid.cellHeight
|
||||||
|
|
||||||
|
property string wallpaperPath: modelData || ""
|
||||||
|
property bool isSelected: getCurrentWallpaper() === modelData
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: wallpaperCard
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingXS
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
|
||||||
|
radius: parent.radius
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CachingImage {
|
||||||
|
id: thumbnailImage
|
||||||
|
anchors.fill: parent
|
||||||
|
imagePath: modelData || ""
|
||||||
|
maxCacheSize: 256
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
maskEnabled: true
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1.0
|
||||||
|
maskSource: ShaderEffectSource {
|
||||||
|
sourceItem: Rectangle {
|
||||||
|
width: thumbnailImage.width
|
||||||
|
height: thumbnailImage.height
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
anchors.fill: parent
|
||||||
|
cornerRadius: parent.radius
|
||||||
|
stateColor: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: wallpaperMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
gridIndex = index;
|
||||||
|
if (modelData) {
|
||||||
|
setCurrentWallpaper(modelData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: wallpaperFolderModel.count === 0
|
||||||
|
text: "No wallpapers found\n\nClick the folder icon below to browse"
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: Theme.outline
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: (parent.width - controlsRow.width - Theme.spacingS) / 2
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: controlsRow
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "skip_previous"
|
||||||
|
iconSize: 20
|
||||||
|
buttonSize: 32
|
||||||
|
enabled: currentPage > 0
|
||||||
|
opacity: enabled ? 1.0 : 0.3
|
||||||
|
onClicked: {
|
||||||
|
if (currentPage > 0) {
|
||||||
|
currentPage--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: wallpaperFolderModel.count > 0 ? `${wallpaperFolderModel.count} wallpapers • ${currentPage + 1} / ${totalPages}` : "No wallpapers"
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "skip_next"
|
||||||
|
iconSize: 20
|
||||||
|
buttonSize: 32
|
||||||
|
enabled: currentPage < totalPages - 1
|
||||||
|
opacity: enabled ? 1.0 : 0.3
|
||||||
|
onClicked: {
|
||||||
|
if (currentPage < totalPages - 1) {
|
||||||
|
currentPage++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
height: 18
|
||||||
|
text: selectedFileName
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.5
|
||||||
|
visible: selectedFileName !== ""
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
642
quickshell/.config/quickshell/Modules/DankDash/WeatherTab.qml
Normal file
642
quickshell/.config/quickshell/Modules/DankDash/WeatherTab.qml
Normal file
|
|
@ -0,0 +1,642 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
implicitWidth: 700
|
||||||
|
implicitHeight: 410
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "cloud_off"
|
||||||
|
size: Theme.iconSize * 2
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "No Weather Data Available"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 70
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: refreshButton
|
||||||
|
name: "refresh"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
|
||||||
|
property bool isRefreshing: false
|
||||||
|
enabled: !isRefreshing
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
|
||||||
|
onClicked: {
|
||||||
|
refreshButton.isRefreshing = true
|
||||||
|
WeatherService.forceRefresh()
|
||||||
|
refreshTimer.restart()
|
||||||
|
}
|
||||||
|
enabled: parent.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: refreshTimer
|
||||||
|
interval: 2000
|
||||||
|
onTriggered: refreshButton.isRefreshing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation on rotation {
|
||||||
|
running: refreshButton.isRefreshing
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
loops: Animation.Infinite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: weatherIcon.width + tempColumn.width + sunriseColumn.width + Theme.spacingM * 2
|
||||||
|
height: 70
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: weatherIcon
|
||||||
|
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
|
||||||
|
size: Theme.iconSize * 1.5
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
shadowEnabled: true
|
||||||
|
shadowHorizontalOffset: 0
|
||||||
|
shadowVerticalOffset: 4
|
||||||
|
shadowBlur: 0.8
|
||||||
|
shadowColor: Qt.rgba(0, 0, 0, 0.2)
|
||||||
|
shadowOpacity: 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: tempColumn
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
anchors.left: weatherIcon.right
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: tempText.width + unitText.width + Theme.spacingXS
|
||||||
|
height: tempText.height
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: tempText
|
||||||
|
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge + 4
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Light
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: unitText
|
||||||
|
text: SettingsData.useFahrenheit ? "F" : "C"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.left: tempText.right
|
||||||
|
anchors.leftMargin: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (WeatherService.weather.available) {
|
||||||
|
SettingsData.setTemperatureUnit(!SettingsData.useFahrenheit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enabled: WeatherService.weather.available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: WeatherService.weather.city || ""
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: sunriseColumn
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
anchors.left: tempColumn.right
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: WeatherService.weather.sunrise && WeatherService.weather.sunset
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: sunriseIcon.width + sunriseText.width + Theme.spacingXS
|
||||||
|
height: sunriseIcon.height
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: sunriseIcon
|
||||||
|
name: "wb_twilight"
|
||||||
|
size: Theme.iconSize - 6
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: sunriseText
|
||||||
|
text: WeatherService.weather.sunrise || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||||
|
anchors.left: sunriseIcon.right
|
||||||
|
anchors.leftMargin: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: sunsetIcon.width + sunsetText.width + Theme.spacingXS
|
||||||
|
height: sunsetIcon.height
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: sunsetIcon
|
||||||
|
name: "bedtime"
|
||||||
|
size: Theme.iconSize - 6
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: sunsetText
|
||||||
|
text: WeatherService.weather.sunset || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||||
|
anchors.left: sunsetIcon.right
|
||||||
|
anchors.leftMargin: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
width: parent.width
|
||||||
|
height: 95
|
||||||
|
columns: 6
|
||||||
|
columnSpacing: Theme.spacingS
|
||||||
|
rowSpacing: 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "device_thermostat"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Feels Like"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: (SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)) + "°"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall + 1
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "humidity_low"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Humidity"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall + 1
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "air"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Wind"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: WeatherService.weather.wind || "--"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall + 1
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "speed"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Pressure"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: WeatherService.weather.pressure ? WeatherService.weather.pressure + " hPa" : "--"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall + 1
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "rainy"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Rain Chance"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: WeatherService.weather.precipitationProbability ? WeatherService.weather.precipitationProbability + "%" : "0%"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall + 1
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "wb_sunny"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Visibility"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Good"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall + 1
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 70 - 95 - Theme.spacingM * 3 - 2
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "7-Day Forecast"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - Theme.fontSizeMedium - Theme.spacingS - Theme.spacingL
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: 7
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: (parent.width - Theme.spacingXS * 6) / 7
|
||||||
|
height: parent.height
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
property var dayDate: {
|
||||||
|
const date = new Date()
|
||||||
|
date.setDate(date.getDate() + index)
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
property bool isToday: index === 0
|
||||||
|
property var forecastData: {
|
||||||
|
if (WeatherService.weather.forecast && WeatherService.weather.forecast.length > index) {
|
||||||
|
return WeatherService.weather.forecast[index]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
|
||||||
|
border.color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
|
||||||
|
border.width: isToday ? 1 : 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: Qt.locale().dayName(dayDate.getDay(), Locale.ShortFormat)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: isToday ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: isToday ? Font.Medium : Font.Normal
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: forecastData ? WeatherService.getWeatherIcon(forecastData.wCode || 0) : "cloud"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: isToday ? Theme.primary : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 2
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: forecastData ? (SettingsData.useFahrenheit ? (forecastData.tempMaxF || forecastData.tempMax) : (forecastData.tempMax || 0)) + "°/" + (SettingsData.useFahrenheit ? (forecastData.tempMinF || forecastData.tempMin) : (forecastData.tempMin || 0)) + "°" : "--/--"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: isToday ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 1
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
visible: forecastData && forecastData.sunrise && forecastData.sunset
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 2
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "wb_twilight"
|
||||||
|
size: 8
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: forecastData ? forecastData.sunrise : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 2
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 2
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "bedtime"
|
||||||
|
size: 8
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: forecastData ? forecastData.sunset : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 2
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: customButtonKeyboard
|
||||||
|
circular: false
|
||||||
|
property string text: ""
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
property bool isShift: false
|
||||||
|
color: Theme.surface
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: contentItem
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: parent.text
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
font.weight: Font.Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
361
quickshell/.config/quickshell/Modules/Lock/Keyboard.qml
Normal file
361
quickshell/.config/quickshell/Modules/Lock/Keyboard.qml
Normal file
|
|
@ -0,0 +1,361 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
property Item target
|
||||||
|
height: 60 * 5
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
color: Theme.widgetBackground()
|
||||||
|
|
||||||
|
property double rowSpacing: 0.01 * width // horizontal spacing between keyboard
|
||||||
|
property double columnSpacing: 0.02 * height // vertical spacing between keyboard
|
||||||
|
property bool shift: false //Boolean for the shift state
|
||||||
|
property bool symbols: false //Boolean for the symbol state
|
||||||
|
property double columns: 10 // Number of column
|
||||||
|
property double rows: 4 // Number of row
|
||||||
|
|
||||||
|
property string strShift: '\u2191' // UPWARDS ARROW unicode
|
||||||
|
property string strBackspace: "Backspace"
|
||||||
|
|
||||||
|
property var modelKeyboard: {
|
||||||
|
"row_1": [
|
||||||
|
{
|
||||||
|
text: 'q',
|
||||||
|
symbol: '1',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'w',
|
||||||
|
symbol: '2',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'e',
|
||||||
|
symbol: '3',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'r',
|
||||||
|
symbol: '4',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 't',
|
||||||
|
symbol: '5',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'y',
|
||||||
|
symbol: '6',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'u',
|
||||||
|
symbol: '7',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'i',
|
||||||
|
symbol: '8',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'o',
|
||||||
|
symbol: '9',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'p',
|
||||||
|
symbol: '0',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"row_2": [
|
||||||
|
{
|
||||||
|
text: 'a',
|
||||||
|
symbol: '-',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 's',
|
||||||
|
symbol: '/',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'd',
|
||||||
|
symbol: ':',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'f',
|
||||||
|
symbol: ';',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'g',
|
||||||
|
symbol: '(',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'h',
|
||||||
|
symbol: ')',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'j',
|
||||||
|
symbol: '€',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'k',
|
||||||
|
symbol: '&',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'l',
|
||||||
|
symbol: '@',
|
||||||
|
width: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_3": [
|
||||||
|
{
|
||||||
|
text: strShift,
|
||||||
|
symbol: strShift,
|
||||||
|
width: 1.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'z',
|
||||||
|
symbol: '.',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'x',
|
||||||
|
symbol: ',',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'c',
|
||||||
|
symbol: '?',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'v',
|
||||||
|
symbol: '!',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'b',
|
||||||
|
symbol: "'",
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'n',
|
||||||
|
symbol: "%",
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'm',
|
||||||
|
symbol: '"',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "'",
|
||||||
|
symbol: "*",
|
||||||
|
width: 1.5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_4": [
|
||||||
|
{
|
||||||
|
text: '123',
|
||||||
|
symbol: 'ABC',
|
||||||
|
width: 1.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: ' ',
|
||||||
|
symbol: ' ',
|
||||||
|
width: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '.',
|
||||||
|
symbol: '.',
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: strBackspace,
|
||||||
|
symbol: strBackspace,
|
||||||
|
width: 1.5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
//Here is the corresponding table between the ascii and the key event
|
||||||
|
property var tableKeyEvent: {
|
||||||
|
"_0": Qt.Key_0,
|
||||||
|
"_1": Qt.Key_1,
|
||||||
|
"_2": Qt.Key_2,
|
||||||
|
"_3": Qt.Key_3,
|
||||||
|
"_4": Qt.Key_4,
|
||||||
|
"_5": Qt.Key_5,
|
||||||
|
"_6": Qt.Key_6,
|
||||||
|
"_7": Qt.Key_7,
|
||||||
|
"_8": Qt.Key_8,
|
||||||
|
"_9": Qt.Key_9,
|
||||||
|
"_a": Qt.Key_A,
|
||||||
|
"_b": Qt.Key_B,
|
||||||
|
"_c": Qt.Key_C,
|
||||||
|
"_d": Qt.Key_D,
|
||||||
|
"_e": Qt.Key_E,
|
||||||
|
"_f": Qt.Key_F,
|
||||||
|
"_g": Qt.Key_G,
|
||||||
|
"_h": Qt.Key_H,
|
||||||
|
"_i": Qt.Key_I,
|
||||||
|
"_j": Qt.Key_J,
|
||||||
|
"_k": Qt.Key_K,
|
||||||
|
"_l": Qt.Key_L,
|
||||||
|
"_m": Qt.Key_M,
|
||||||
|
"_n": Qt.Key_N,
|
||||||
|
"_o": Qt.Key_O,
|
||||||
|
"_p": Qt.Key_P,
|
||||||
|
"_q": Qt.Key_Q,
|
||||||
|
"_r": Qt.Key_R,
|
||||||
|
"_s": Qt.Key_S,
|
||||||
|
"_t": Qt.Key_T,
|
||||||
|
"_u": Qt.Key_U,
|
||||||
|
"_v": Qt.Key_V,
|
||||||
|
"_w": Qt.Key_W,
|
||||||
|
"_x": Qt.Key_X,
|
||||||
|
"_y": Qt.Key_Y,
|
||||||
|
"_z": Qt.Key_Z,
|
||||||
|
"_\u2190": Qt.Key_Backspace,
|
||||||
|
"_return": Qt.Key_Return,
|
||||||
|
"_ ": Qt.Key_Space,
|
||||||
|
"_-": Qt.Key_Minus,
|
||||||
|
"_/": Qt.Key_Slash,
|
||||||
|
"_:": Qt.Key_Colon,
|
||||||
|
"_;": Qt.Key_Semicolon,
|
||||||
|
"_(": Qt.Key_BracketLeft,
|
||||||
|
"_)": Qt.Key_BracketRight,
|
||||||
|
"_€": parseInt("20ac", 16) // I didn't find the appropriate Qt event so I used the hex format
|
||||||
|
,
|
||||||
|
"_&": Qt.Key_Ampersand,
|
||||||
|
"_@": Qt.Key_At,
|
||||||
|
'_"': Qt.Key_QuoteDbl,
|
||||||
|
"_.": Qt.Key_Period,
|
||||||
|
"_,": Qt.Key_Comma,
|
||||||
|
"_?": Qt.Key_Question,
|
||||||
|
"_!": Qt.Key_Exclam,
|
||||||
|
"_'": Qt.Key_Apostrophe,
|
||||||
|
"_%": Qt.Key_Percent,
|
||||||
|
"_*": Qt.Key_Asterisk
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: keyboard_container
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 5
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 5
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 5
|
||||||
|
|
||||||
|
//One column which contains 5 rows
|
||||||
|
Column {
|
||||||
|
spacing: columnSpacing
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: row_1
|
||||||
|
spacing: rowSpacing
|
||||||
|
Repeater {
|
||||||
|
model: modelKeyboard["row_1"]
|
||||||
|
delegate: CustomButtonKeyboard {
|
||||||
|
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
||||||
|
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||||
|
height: keyboard_container.height / rows - columnSpacing
|
||||||
|
|
||||||
|
onClicked: root.clicked(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
id: row_2
|
||||||
|
spacing: rowSpacing
|
||||||
|
Repeater {
|
||||||
|
model: modelKeyboard["row_2"]
|
||||||
|
delegate: CustomButtonKeyboard {
|
||||||
|
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
||||||
|
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||||
|
height: keyboard_container.height / rows - columnSpacing
|
||||||
|
|
||||||
|
onClicked: root.clicked(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
id: row_3
|
||||||
|
spacing: rowSpacing
|
||||||
|
Repeater {
|
||||||
|
model: modelKeyboard["row_3"]
|
||||||
|
delegate: CustomButtonKeyboard {
|
||||||
|
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
||||||
|
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||||
|
height: keyboard_container.height / rows - columnSpacing
|
||||||
|
isShift: shift && text === strShift
|
||||||
|
|
||||||
|
onClicked: root.clicked(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
id: row_4
|
||||||
|
spacing: rowSpacing
|
||||||
|
Repeater {
|
||||||
|
model: modelKeyboard["row_4"]
|
||||||
|
delegate: CustomButtonKeyboard {
|
||||||
|
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
||||||
|
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||||
|
height: keyboard_container.height / rows - columnSpacing
|
||||||
|
|
||||||
|
onClicked: root.clicked(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signal clicked(string text)
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onClicked(text) {
|
||||||
|
if (!keyboard_controller.target)
|
||||||
|
return;
|
||||||
|
if (text === strShift) {
|
||||||
|
root.shift = !root.shift; // toggle shift
|
||||||
|
} else if (text === '123') {
|
||||||
|
root.symbols = true;
|
||||||
|
} else if (text === 'ABC') {
|
||||||
|
root.symbols = false;
|
||||||
|
} else {
|
||||||
|
// insert text into target
|
||||||
|
if (text === strBackspace) {
|
||||||
|
var current = keyboard_controller.target.text;
|
||||||
|
keyboard_controller.target.text = current.slice(0, current.length - 1);
|
||||||
|
} else {
|
||||||
|
// normal character
|
||||||
|
var charToInsert = root.symbols ? text : (root.shift ? text.toUpperCase() : text);
|
||||||
|
var current = keyboard_controller.target.text;
|
||||||
|
var cursorPos = keyboard_controller.target.cursorPosition;
|
||||||
|
keyboard_controller.target.text = current.slice(0, cursorPos) + charToInsert + current.slice(cursorPos);
|
||||||
|
keyboard_controller.target.cursorPosition = cursorPos + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// shift is momentary
|
||||||
|
if (root.shift && text !== strShift)
|
||||||
|
root.shift = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: keyboard_controller
|
||||||
|
|
||||||
|
// reference on the TextInput
|
||||||
|
property Item target
|
||||||
|
//Booléan on the state of the keyboard
|
||||||
|
property bool isKeyboardActive: false
|
||||||
|
|
||||||
|
property var rootObject
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
if (!isKeyboardActive && keyboard === null) {
|
||||||
|
keyboard = keyboardComponent.createObject(keyboard_controller.rootObject);
|
||||||
|
keyboard.target = keyboard_controller.target;
|
||||||
|
isKeyboardActive = true;
|
||||||
|
} else
|
||||||
|
console.info("The keyboard is already shown");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
if (isKeyboardActive && keyboard !== null) {
|
||||||
|
keyboard.destroy();
|
||||||
|
isKeyboardActive = false;
|
||||||
|
} else
|
||||||
|
console.info("The keyboard is already hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
// private
|
||||||
|
property Item keyboard: null
|
||||||
|
Component {
|
||||||
|
id: keyboardComponent
|
||||||
|
Keyboard {}
|
||||||
|
}
|
||||||
|
}
|
||||||
153
quickshell/.config/quickshell/Modules/Lock/Lock.qml
Normal file
153
quickshell/.config/quickshell/Modules/Lock/Lock.qml
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
property string sid: Quickshell.env("XDG_SESSION_ID") || "self"
|
||||||
|
property string sessionPath: ""
|
||||||
|
|
||||||
|
function activate() {
|
||||||
|
loader.activeAsync = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
getSessionPath.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: getSessionPath
|
||||||
|
command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", "/org/freedesktop/login1", "--method", "org.freedesktop.login1.Manager.GetSession", sid]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const match = text.match(/objectpath '([^']+)'/)
|
||||||
|
if (match) {
|
||||||
|
root.sessionPath = match[1]
|
||||||
|
console.log("Found session path:", root.sessionPath)
|
||||||
|
checkCurrentLockState.running = true
|
||||||
|
lockStateMonitor.running = true
|
||||||
|
} else {
|
||||||
|
console.warn("Could not determine session path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.warn("Failed to get session path, exit code:", exitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: checkCurrentLockState
|
||||||
|
command: root.sessionPath ? ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.login1.Session", "LockedHint"] : []
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text.includes("true")) {
|
||||||
|
console.log("Session is locked on startup, activating lock screen")
|
||||||
|
LockScreenService.resetState()
|
||||||
|
loader.activeAsync = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.warn("Failed to check initial lock state, exit code:", exitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: lockStateMonitor
|
||||||
|
command: root.sessionPath ? ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath] : []
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
|
||||||
|
onRead: line => {
|
||||||
|
if (line.includes("org.freedesktop.login1.Session.Lock")) {
|
||||||
|
console.log("login1: Lock signal received -> show lock")
|
||||||
|
LockScreenService.resetState()
|
||||||
|
loader.activeAsync = true
|
||||||
|
} else if (line.includes("org.freedesktop.login1.Session.Unlock")) {
|
||||||
|
console.log("login1: Unlock signal received -> hide lock")
|
||||||
|
loader.active = false
|
||||||
|
} else if (line.includes("LockedHint") && line.includes("true")) {
|
||||||
|
console.log("login1: LockedHint=true -> show lock")
|
||||||
|
LockScreenService.resetState()
|
||||||
|
loader.activeAsync = true
|
||||||
|
} else if (line.includes("LockedHint") && line.includes("false")) {
|
||||||
|
console.log("login1: LockedHint=false -> hide lock")
|
||||||
|
loader.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.warn("gdbus monitor failed, exit code:", exitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: loader
|
||||||
|
|
||||||
|
WlSessionLock {
|
||||||
|
id: sessionLock
|
||||||
|
|
||||||
|
property bool unlocked: false
|
||||||
|
property string sharedPasswordBuffer: ""
|
||||||
|
|
||||||
|
locked: true
|
||||||
|
|
||||||
|
onLockedChanged: {
|
||||||
|
if (!locked) {
|
||||||
|
loader.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LockSurface {
|
||||||
|
id: lockSurface
|
||||||
|
lock: sessionLock
|
||||||
|
sharedPasswordBuffer: sessionLock.sharedPasswordBuffer
|
||||||
|
onPasswordChanged: newPassword => {
|
||||||
|
sessionLock.sharedPasswordBuffer = newPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LockScreenDemo {
|
||||||
|
id: demoWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "lock"
|
||||||
|
|
||||||
|
function lock() {
|
||||||
|
console.log("Lock screen requested via IPC")
|
||||||
|
LockScreenService.resetState()
|
||||||
|
loader.activeAsync = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function demo() {
|
||||||
|
console.log("Lock screen DEMO mode requested via IPC")
|
||||||
|
demoWindow.showDemo()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLocked(): bool {
|
||||||
|
return loader.active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1010
quickshell/.config/quickshell/Modules/Lock/LockScreenContent.qml
Normal file
1010
quickshell/.config/quickshell/Modules/Lock/LockScreenContent.qml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,45 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool demoActive: false
|
||||||
|
|
||||||
|
visible: demoActive
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
bottom: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
function showDemo(): void {
|
||||||
|
console.log("Showing lock screen demo")
|
||||||
|
demoActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDemo(): void {
|
||||||
|
console.log("Hiding lock screen demo")
|
||||||
|
demoActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
active: demoActive
|
||||||
|
sourceComponent: LockScreenContent {
|
||||||
|
demoMode: true
|
||||||
|
screenName: root.screen?.name ?? ""
|
||||||
|
onUnlockRequested: root.hideDemo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
quickshell/.config/quickshell/Modules/Lock/LockSurface.qml
Normal file
37
quickshell/.config/quickshell/Modules/Lock/LockSurface.qml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
WlSessionLockSurface {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property WlSessionLock lock
|
||||||
|
required property string sharedPasswordBuffer
|
||||||
|
|
||||||
|
signal passwordChanged(string newPassword)
|
||||||
|
|
||||||
|
readonly property bool locked: lock && !lock.locked
|
||||||
|
|
||||||
|
function unlock(): void {
|
||||||
|
lock.locked = false
|
||||||
|
}
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
sourceComponent: LockScreenContent {
|
||||||
|
demoMode: false
|
||||||
|
passwordBuffer: root.sharedPasswordBuffer
|
||||||
|
screenName: root.screen?.name ?? ""
|
||||||
|
onUnlockRequested: root.unlock()
|
||||||
|
onPasswordBufferChanged: {
|
||||||
|
if (root.sharedPasswordBuffer !== passwordBuffer) {
|
||||||
|
root.passwordChanged(passwordBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
id: listView
|
||||||
|
|
||||||
|
property var keyboardController: null
|
||||||
|
property bool keyboardActive: false
|
||||||
|
property bool autoScrollDisabled: false
|
||||||
|
property alias count: listView.count
|
||||||
|
property alias listContentHeight: listView.contentHeight
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
model: NotificationService.groupedNotifications
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
onIsUserScrollingChanged: {
|
||||||
|
if (isUserScrolling && keyboardController && keyboardController.keyboardNavigationActive) {
|
||||||
|
autoScrollDisabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableAutoScroll() {
|
||||||
|
autoScrollDisabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: positionPreservationTimer
|
||||||
|
interval: 200
|
||||||
|
running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
|
||||||
|
keyboardController.ensureVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationEmptyState {
|
||||||
|
visible: listView.count === 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
onModelChanged: {
|
||||||
|
if (!keyboardController || !keyboardController.keyboardNavigationActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyboardController.rebuildFlatNavigation()
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
|
||||||
|
keyboardController.ensureVisible()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
readonly property bool isExpanded: (NotificationService.expandedGroups[modelData && modelData.key] || false)
|
||||||
|
|
||||||
|
width: ListView.view.width
|
||||||
|
height: notificationCard.height
|
||||||
|
|
||||||
|
NotificationCard {
|
||||||
|
id: notificationCard
|
||||||
|
width: parent.width
|
||||||
|
notificationGroup: modelData
|
||||||
|
keyboardNavigationActive: listView.keyboardActive
|
||||||
|
|
||||||
|
isGroupSelected: {
|
||||||
|
if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
keyboardController.selectionVersion
|
||||||
|
const selection = keyboardController.getCurrentSelection()
|
||||||
|
return selection.type === "group" && selection.groupIndex === index
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedNotificationIndex: {
|
||||||
|
if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
keyboardController.selectionVersion
|
||||||
|
const selection = keyboardController.getCurrentSelection()
|
||||||
|
return (selection.type === "notification" && selection.groupIndex === index) ? selection.notificationIndex : -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: NotificationService
|
||||||
|
|
||||||
|
function onGroupedNotificationsChanged() {
|
||||||
|
if (!keyboardController) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboardController.isTogglingGroup) {
|
||||||
|
keyboardController.rebuildFlatNavigation()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardController.rebuildFlatNavigation()
|
||||||
|
|
||||||
|
if (keyboardController.keyboardNavigationActive) {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (!autoScrollDisabled) {
|
||||||
|
keyboardController.ensureVisible()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExpandedGroupsChanged() {
|
||||||
|
if (keyboardController && keyboardController.keyboardNavigationActive) {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (!autoScrollDisabled) {
|
||||||
|
keyboardController.ensureVisible()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExpandedMessagesChanged() {
|
||||||
|
if (keyboardController && keyboardController.keyboardNavigationActive) {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (!autoScrollDisabled) {
|
||||||
|
keyboardController.ensureVisible()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,728 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var notificationGroup
|
||||||
|
property bool expanded: (NotificationService.expandedGroups[notificationGroup && notificationGroup.key] || false)
|
||||||
|
property bool descriptionExpanded: (NotificationService.expandedMessages[(notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification
|
||||||
|
&& notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""] || false)
|
||||||
|
property bool userInitiatedExpansion: false
|
||||||
|
|
||||||
|
property bool isGroupSelected: false
|
||||||
|
property int selectedNotificationIndex: -1
|
||||||
|
property bool keyboardNavigationActive: false
|
||||||
|
|
||||||
|
width: parent ? parent.width : 400
|
||||||
|
height: {
|
||||||
|
if (expanded) {
|
||||||
|
return expandedContent.height + 28
|
||||||
|
}
|
||||||
|
const baseHeight = 116
|
||||||
|
if (descriptionExpanded) {
|
||||||
|
return baseHeight + descriptionText.contentHeight - (descriptionText.font.pixelSize * 1.2 * 2)
|
||||||
|
}
|
||||||
|
return baseHeight
|
||||||
|
}
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
color: {
|
||||||
|
if (isGroupSelected && keyboardNavigationActive) {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
|
||||||
|
}
|
||||||
|
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12)
|
||||||
|
}
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
|
||||||
|
}
|
||||||
|
border.color: {
|
||||||
|
if (isGroupSelected && keyboardNavigationActive) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5)
|
||||||
|
}
|
||||||
|
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||||
|
}
|
||||||
|
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||||
|
}
|
||||||
|
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||||
|
}
|
||||||
|
border.width: {
|
||||||
|
if (isGroupSelected && keyboardNavigationActive) {
|
||||||
|
return 1.5
|
||||||
|
}
|
||||||
|
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: parent.radius
|
||||||
|
visible: notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Horizontal
|
||||||
|
GradientStop {
|
||||||
|
position: 0.0
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
GradientStop {
|
||||||
|
position: 0.02
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
GradientStop {
|
||||||
|
position: 0.021
|
||||||
|
color: "transparent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opacity: 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: collapsedContent
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: 12
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.rightMargin: 56
|
||||||
|
height: 92
|
||||||
|
visible: !expanded
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: iconContainer
|
||||||
|
readonly property bool hasNotificationImage: notificationGroup?.latestNotification?.image && notificationGroup.latestNotification.image !== ""
|
||||||
|
|
||||||
|
width: 55
|
||||||
|
height: 55
|
||||||
|
radius: 27.5
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||||
|
border.color: "transparent"
|
||||||
|
border.width: 0
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 18
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
source: {
|
||||||
|
if (parent.hasNotificationImage)
|
||||||
|
return notificationGroup.latestNotification.cleanImage
|
||||||
|
if (notificationGroup?.latestNotification?.appIcon) {
|
||||||
|
const appIcon = notificationGroup.latestNotification.appIcon
|
||||||
|
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
|
||||||
|
return appIcon
|
||||||
|
return Quickshell.iconPath(appIcon, true)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !parent.hasNotificationImage && (!notificationGroup?.latestNotification?.appIcon || notificationGroup.latestNotification.appIcon === "")
|
||||||
|
text: {
|
||||||
|
const appName = notificationGroup?.appName || "?"
|
||||||
|
return appName.charAt(0).toUpperCase()
|
||||||
|
}
|
||||||
|
font.pixelSize: 20
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.primaryText
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 18
|
||||||
|
height: 18
|
||||||
|
radius: 9
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: -2
|
||||||
|
anchors.rightMargin: -2
|
||||||
|
visible: (notificationGroup?.count || 0) > 1
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString()
|
||||||
|
color: Theme.primaryText
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: textContainer
|
||||||
|
|
||||||
|
anchors.left: iconContainer.right
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 0
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: -2
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: {
|
||||||
|
const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || ""
|
||||||
|
const appName = (notificationGroup && notificationGroup.appName) || ""
|
||||||
|
return timeStr.length > 0 ? `${appName} • ${timeStr}` : appName
|
||||||
|
}
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.summary) || ""
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: descriptionText
|
||||||
|
property string fullText: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.htmlBody) || ""
|
||||||
|
property bool hasMoreText: truncated
|
||||||
|
|
||||||
|
text: fullText
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: descriptionExpanded ? -1 : 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
visible: text.length > 0
|
||||||
|
linkColor: Theme.primary
|
||||||
|
onLinkActivated: link => Qt.openUrlExternally(link)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (parent.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) {
|
||||||
|
const messageId = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification
|
||||||
|
&& notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""
|
||||||
|
NotificationService.toggleMessageExpansion(messageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
propagateComposedEvents: true
|
||||||
|
onPressed: mouse => {
|
||||||
|
if (parent.hoveredLink) {
|
||||||
|
mouse.accepted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: mouse => {
|
||||||
|
if (parent.hoveredLink) {
|
||||||
|
mouse.accepted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: expandedContent
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: 14
|
||||||
|
anchors.bottomMargin: 14
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.rightMargin: Theme.spacingL
|
||||||
|
spacing: -1
|
||||||
|
visible: expanded
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 56
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: notificationGroup?.appName || ""
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Bold
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 18
|
||||||
|
height: 18
|
||||||
|
radius: 9
|
||||||
|
color: Theme.primary
|
||||||
|
visible: (notificationGroup?.count || 0) > 1
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString()
|
||||||
|
color: Theme.primaryText
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: notificationGroup?.notifications?.slice(0, 10) || []
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id] || false
|
||||||
|
readonly property bool isSelected: root.selectedNotificationIndex === index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: {
|
||||||
|
const baseHeight = 120
|
||||||
|
if (messageExpanded) {
|
||||||
|
const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2
|
||||||
|
if (bodyText.implicitHeight > twoLineHeight + 2) {
|
||||||
|
const extraHeight = bodyText.implicitHeight - twoLineHeight
|
||||||
|
return baseHeight + extraHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baseHeight
|
||||||
|
}
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: isSelected ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.25) : "transparent"
|
||||||
|
border.color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||||
|
border.width: isSelected ? 1 : 1
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: messageIcon
|
||||||
|
|
||||||
|
readonly property bool hasNotificationImage: modelData?.image && modelData.image !== ""
|
||||||
|
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 32
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||||
|
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 1
|
||||||
|
source: {
|
||||||
|
if (parent.hasNotificationImage)
|
||||||
|
return modelData.cleanImage
|
||||||
|
|
||||||
|
if (modelData?.appIcon) {
|
||||||
|
const appIcon = modelData.appIcon
|
||||||
|
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
|
||||||
|
return appIcon
|
||||||
|
|
||||||
|
return Quickshell.iconPath(appIcon, true)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !parent.hasNotificationImage && (!modelData?.appIcon || modelData.appIcon === "")
|
||||||
|
text: {
|
||||||
|
const appName = modelData?.appName || "?"
|
||||||
|
return appName.charAt(0).toUpperCase()
|
||||||
|
}
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.primaryText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.left: messageIcon.right
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: buttonArea.top
|
||||||
|
anchors.bottomMargin: 4
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: modelData?.timeStr || ""
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: modelData?.summary || ""
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: bodyText
|
||||||
|
property bool hasMoreText: truncated
|
||||||
|
|
||||||
|
text: modelData?.htmlBody || ""
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
width: parent.width
|
||||||
|
elide: messageExpanded ? Text.ElideNone : Text.ElideRight
|
||||||
|
maximumLineCount: messageExpanded ? -1 : 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
visible: text.length > 0
|
||||||
|
linkColor: Theme.primary
|
||||||
|
onLinkActivated: link => Qt.openUrlExternally(link)
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (bodyText.hasMoreText || messageExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) {
|
||||||
|
NotificationService.toggleMessageExpansion(modelData?.notification?.id || "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
propagateComposedEvents: true
|
||||||
|
onPressed: mouse => {
|
||||||
|
if (parent.hoveredLink) {
|
||||||
|
mouse.accepted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: mouse => {
|
||||||
|
if (parent.hoveredLink) {
|
||||||
|
mouse.accepted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: buttonArea
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: 30
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: modelData?.actions || []
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isHovered: false
|
||||||
|
|
||||||
|
width: Math.max(actionText.implicitWidth + 12, 50)
|
||||||
|
height: 24
|
||||||
|
radius: 4
|
||||||
|
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: actionText
|
||||||
|
text: {
|
||||||
|
const baseText = modelData.text || "View"
|
||||||
|
if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0)) {
|
||||||
|
return `${baseText} (${index + 1})`
|
||||||
|
}
|
||||||
|
return baseText
|
||||||
|
}
|
||||||
|
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onEntered: parent.isHovered = true
|
||||||
|
onExited: parent.isHovered = false
|
||||||
|
onClicked: {
|
||||||
|
if (modelData && modelData.invoke) {
|
||||||
|
modelData.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isHovered: false
|
||||||
|
|
||||||
|
width: Math.max(clearText.implicitWidth + 12, 50)
|
||||||
|
height: 24
|
||||||
|
radius: 4
|
||||||
|
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: clearText
|
||||||
|
text: "Clear"
|
||||||
|
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onEntered: parent.isHovered = true
|
||||||
|
onExited: parent.isHovered = false
|
||||||
|
onClicked: NotificationService.dismissNotification(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
visible: !expanded
|
||||||
|
anchors.right: clearButton.left
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: notificationGroup?.latestNotification?.actions || []
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isHovered: false
|
||||||
|
|
||||||
|
width: Math.max(actionText.implicitWidth + 12, 50)
|
||||||
|
height: 24
|
||||||
|
radius: 4
|
||||||
|
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: actionText
|
||||||
|
text: {
|
||||||
|
const baseText = modelData.text || "View"
|
||||||
|
if (keyboardNavigationActive && isGroupSelected) {
|
||||||
|
return `${baseText} (${index + 1})`
|
||||||
|
}
|
||||||
|
return baseText
|
||||||
|
}
|
||||||
|
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onEntered: parent.isHovered = true
|
||||||
|
onExited: parent.isHovered = false
|
||||||
|
onClicked: {
|
||||||
|
if (modelData && modelData.invoke) {
|
||||||
|
modelData.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: clearButton
|
||||||
|
|
||||||
|
property bool isHovered: false
|
||||||
|
|
||||||
|
visible: !expanded
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
width: clearText.width + 16
|
||||||
|
height: clearText.height + 8
|
||||||
|
radius: 6
|
||||||
|
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: clearText
|
||||||
|
text: "Clear"
|
||||||
|
color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onEntered: clearButton.isHovered = true
|
||||||
|
onExited: clearButton.isHovered = false
|
||||||
|
onClicked: NotificationService.dismissGroup(notificationGroup?.key || "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded
|
||||||
|
onClicked: {
|
||||||
|
root.userInitiatedExpansion = true
|
||||||
|
NotificationService.toggleGroupExpansion(notificationGroup?.key || "")
|
||||||
|
}
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: fixedControls
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: 12
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
width: 60
|
||||||
|
height: 28
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
visible: (notificationGroup?.count || 0) > 1
|
||||||
|
iconName: expanded ? "expand_less" : "expand_more"
|
||||||
|
iconSize: 18
|
||||||
|
buttonSize: 28
|
||||||
|
onClicked: {
|
||||||
|
root.userInitiatedExpansion = true
|
||||||
|
NotificationService.toggleGroupExpansion(notificationGroup?.key || "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: 18
|
||||||
|
buttonSize: 28
|
||||||
|
onClicked: NotificationService.dismissGroup(notificationGroup?.key || "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
enabled: root.userInitiatedExpansion
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
onFinished: root.userInitiatedExpansion = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.Notifications.Center
|
||||||
|
|
||||||
|
DankPopout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool notificationHistoryVisible: false
|
||||||
|
property string triggerSection: "right"
|
||||||
|
property var triggerScreen: null
|
||||||
|
|
||||||
|
NotificationKeyboardController {
|
||||||
|
id: keyboardController
|
||||||
|
listView: null
|
||||||
|
isOpen: notificationHistoryVisible
|
||||||
|
onClose: () => {
|
||||||
|
notificationHistoryVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTriggerPosition(x, y, width, section, screen) {
|
||||||
|
triggerX = x
|
||||||
|
triggerY = y
|
||||||
|
triggerWidth = width
|
||||||
|
triggerSection = section
|
||||||
|
triggerScreen = screen
|
||||||
|
}
|
||||||
|
|
||||||
|
popupWidth: 400
|
||||||
|
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400
|
||||||
|
triggerX: Screen.width - 400 - Theme.spacingL
|
||||||
|
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
||||||
|
triggerWidth: 40
|
||||||
|
positioning: "center"
|
||||||
|
screen: triggerScreen
|
||||||
|
shouldBeVisible: notificationHistoryVisible
|
||||||
|
visible: shouldBeVisible
|
||||||
|
|
||||||
|
onNotificationHistoryVisibleChanged: {
|
||||||
|
if (notificationHistoryVisible) {
|
||||||
|
open()
|
||||||
|
} else {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onShouldBeVisibleChanged: {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
NotificationService.onOverlayOpen()
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentLoader.item) {
|
||||||
|
contentLoader.item.externalKeyboardController = keyboardController
|
||||||
|
|
||||||
|
const notificationList = findChild(contentLoader.item, "notificationList")
|
||||||
|
const notificationHeader = findChild(contentLoader.item, "notificationHeader")
|
||||||
|
|
||||||
|
if (notificationList) {
|
||||||
|
keyboardController.listView = notificationList
|
||||||
|
notificationList.keyboardController = keyboardController
|
||||||
|
}
|
||||||
|
if (notificationHeader) {
|
||||||
|
notificationHeader.keyboardController = keyboardController
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardController.reset()
|
||||||
|
keyboardController.rebuildFlatNavigation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
NotificationService.onOverlayClose()
|
||||||
|
keyboardController.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findChild(parent, objectName) {
|
||||||
|
if (parent.objectName === objectName) {
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
for (let i = 0; i < parent.children.length; i++) {
|
||||||
|
const child = parent.children[i]
|
||||||
|
const result = findChild(child, objectName)
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Rectangle {
|
||||||
|
id: notificationContent
|
||||||
|
|
||||||
|
property var externalKeyboardController: null
|
||||||
|
property real cachedHeaderHeight: 32
|
||||||
|
|
||||||
|
implicitHeight: {
|
||||||
|
let baseHeight = Theme.spacingL * 2
|
||||||
|
baseHeight += cachedHeaderHeight
|
||||||
|
baseHeight += (notificationSettings.expanded ? notificationSettings.contentHeight : 0)
|
||||||
|
baseHeight += Theme.spacingM * 2
|
||||||
|
let listHeight = notificationList.listContentHeight
|
||||||
|
if (NotificationService.groupedNotifications.length === 0) {
|
||||||
|
listHeight = 200
|
||||||
|
}
|
||||||
|
baseHeight += Math.min(listHeight, 600)
|
||||||
|
const maxHeight = root.screen ? root.screen.height * 0.8 : Screen.height * 0.8
|
||||||
|
return Math.max(300, Math.min(baseHeight, maxHeight))
|
||||||
|
}
|
||||||
|
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (root.shouldBeVisible) {
|
||||||
|
forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
root.close()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (externalKeyboardController) {
|
||||||
|
externalKeyboardController.handleKey(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onShouldBeVisibleChanged() {
|
||||||
|
if (root.shouldBeVisible) {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
notificationContent.forceActiveFocus()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
notificationContent.focus = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target: root
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: contentColumn
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: contentColumnInner
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
NotificationHeader {
|
||||||
|
id: notificationHeader
|
||||||
|
objectName: "notificationHeader"
|
||||||
|
onHeightChanged: notificationContent.cachedHeaderHeight = height
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationSettings {
|
||||||
|
id: notificationSettings
|
||||||
|
expanded: notificationHeader.showSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardNavigatedNotificationList {
|
||||||
|
id: notificationList
|
||||||
|
objectName: "notificationList"
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - notificationContent.cachedHeaderHeight - notificationSettings.height - contentColumnInner.spacing * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationKeyboardHints {
|
||||||
|
id: keyboardHints
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
showHints: (externalKeyboardController && externalKeyboardController.showKeyboardHints) || false
|
||||||
|
z: 200
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 180
|
||||||
|
easing.type: Easing.OutQuart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 200
|
||||||
|
visible: NotificationService.notifications.length === 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
width: parent.width * 0.8
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
name: "notifications_none"
|
||||||
|
size: Theme.iconSizeLarge + 16
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "Nothing to see here"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
|
||||||
|
font.weight: Font.Medium
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var keyboardController: null
|
||||||
|
property bool showSettings: false
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Notifications"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: doNotDisturbButton
|
||||||
|
|
||||||
|
iconName: SessionData.doNotDisturb ? "notifications_off" : "notifications"
|
||||||
|
iconColor: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText
|
||||||
|
buttonSize: 28
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: doNotDisturbTooltip
|
||||||
|
|
||||||
|
width: tooltipText.contentWidth + Theme.spacingS * 2
|
||||||
|
height: tooltipText.contentHeight + Theme.spacingXS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
anchors.bottom: parent.top
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
visible: doNotDisturbButton.children[1].containsMouse
|
||||||
|
opacity: visible ? 1 : 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: tooltipText
|
||||||
|
|
||||||
|
text: "Do Not Disturb"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
font.hintingPreference: Font.PreferFullHinting
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: helpButton
|
||||||
|
iconName: "info"
|
||||||
|
iconColor: (keyboardController && keyboardController.showKeyboardHints) ? Theme.primary : Theme.surfaceText
|
||||||
|
buttonSize: 28
|
||||||
|
visible: keyboardController !== null
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: {
|
||||||
|
if (keyboardController) {
|
||||||
|
keyboardController.showKeyboardHints = !keyboardController.showKeyboardHints
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: settingsButton
|
||||||
|
iconName: "settings"
|
||||||
|
iconColor: root.showSettings ? Theme.primary : Theme.surfaceText
|
||||||
|
buttonSize: 28
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: root.showSettings = !root.showSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: clearAllButton
|
||||||
|
|
||||||
|
width: 120
|
||||||
|
height: 28
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
visible: NotificationService.notifications.length > 0
|
||||||
|
color: clearArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
border.color: clearArea.containsMouse ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "delete_sweep"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Clear All"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: clearArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: NotificationService.clearAllNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,459 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: controller
|
||||||
|
|
||||||
|
property var listView: null
|
||||||
|
property bool isOpen: false
|
||||||
|
property var onClose: null
|
||||||
|
|
||||||
|
property int selectionVersion: 0
|
||||||
|
|
||||||
|
property bool keyboardNavigationActive: false
|
||||||
|
property int selectedFlatIndex: 0
|
||||||
|
property var flatNavigation: []
|
||||||
|
property bool showKeyboardHints: false
|
||||||
|
|
||||||
|
property string selectedNotificationId: ""
|
||||||
|
property string selectedGroupKey: ""
|
||||||
|
property string selectedItemType: ""
|
||||||
|
property bool isTogglingGroup: false
|
||||||
|
property bool isRebuilding: false
|
||||||
|
|
||||||
|
function rebuildFlatNavigation() {
|
||||||
|
isRebuilding = true
|
||||||
|
|
||||||
|
const nav = []
|
||||||
|
const groups = NotificationService.groupedNotifications
|
||||||
|
|
||||||
|
for (let i = 0; i < groups.length; i++) {
|
||||||
|
const group = groups[i]
|
||||||
|
const isExpanded = NotificationService.expandedGroups[group.key] || false
|
||||||
|
|
||||||
|
nav.push({
|
||||||
|
"type": "group",
|
||||||
|
"groupIndex": i,
|
||||||
|
"notificationIndex": -1,
|
||||||
|
"groupKey": group.key,
|
||||||
|
"notificationId": ""
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isExpanded) {
|
||||||
|
const notifications = group.notifications || []
|
||||||
|
for (let j = 0; j < notifications.length; j++) {
|
||||||
|
const notifId = String(notifications[j] && notifications[j].notification && notifications[j].notification.id ? notifications[j].notification.id : "")
|
||||||
|
nav.push({
|
||||||
|
"type": "notification",
|
||||||
|
"groupIndex": i,
|
||||||
|
"notificationIndex": j,
|
||||||
|
"groupKey": group.key,
|
||||||
|
"notificationId": notifId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flatNavigation = nav
|
||||||
|
updateSelectedIndexFromId()
|
||||||
|
isRebuilding = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedIndexFromId() {
|
||||||
|
if (!keyboardNavigationActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < flatNavigation.length; i++) {
|
||||||
|
const item = flatNavigation[i]
|
||||||
|
|
||||||
|
if (selectedItemType === "group" && item.type === "group" && item.groupKey === selectedGroupKey) {
|
||||||
|
selectedFlatIndex = i
|
||||||
|
selectionVersion++ // Trigger UI update
|
||||||
|
return
|
||||||
|
} else if (selectedItemType === "notification" && item.type === "notification" && String(item.notificationId) === String(selectedNotificationId)) {
|
||||||
|
selectedFlatIndex = i
|
||||||
|
selectionVersion++ // Trigger UI update
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, try to find the same group but select the group header instead
|
||||||
|
if (selectedItemType === "notification") {
|
||||||
|
for (let j = 0; j < flatNavigation.length; j++) {
|
||||||
|
const groupItem = flatNavigation[j]
|
||||||
|
if (groupItem.type === "group" && groupItem.groupKey === selectedGroupKey) {
|
||||||
|
selectedFlatIndex = j
|
||||||
|
selectedItemType = "group"
|
||||||
|
selectedNotificationId = ""
|
||||||
|
selectionVersion++ // Trigger UI update
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still not found, clamp to valid range and update
|
||||||
|
if (flatNavigation.length > 0) {
|
||||||
|
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1)
|
||||||
|
selectedFlatIndex = Math.max(selectedFlatIndex, 0)
|
||||||
|
updateSelectedIdFromIndex()
|
||||||
|
selectionVersion++ // Trigger UI update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedIdFromIndex() {
|
||||||
|
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatNavigation.length) {
|
||||||
|
const item = flatNavigation[selectedFlatIndex]
|
||||||
|
selectedItemType = item.type
|
||||||
|
selectedGroupKey = item.groupKey
|
||||||
|
selectedNotificationId = item.notificationId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
selectedFlatIndex = 0
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
showKeyboardHints = false
|
||||||
|
// Reset keyboardActive when modal is reset
|
||||||
|
if (listView) {
|
||||||
|
listView.keyboardActive = false
|
||||||
|
}
|
||||||
|
rebuildFlatNavigation()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNext() {
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
if (flatNavigation.length === 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Re-enable auto-scrolling when arrow keys are used
|
||||||
|
if (listView && listView.enableAutoScroll) {
|
||||||
|
listView.enableAutoScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFlatIndex = Math.min(selectedFlatIndex + 1, flatNavigation.length - 1)
|
||||||
|
updateSelectedIdFromIndex()
|
||||||
|
selectionVersion++
|
||||||
|
ensureVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNextWrapping() {
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
if (flatNavigation.length === 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Re-enable auto-scrolling when arrow keys are used
|
||||||
|
if (listView && listView.enableAutoScroll) {
|
||||||
|
listView.enableAutoScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFlatIndex = (selectedFlatIndex + 1) % flatNavigation.length
|
||||||
|
updateSelectedIdFromIndex()
|
||||||
|
selectionVersion++
|
||||||
|
ensureVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrevious() {
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
if (flatNavigation.length === 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Re-enable auto-scrolling when arrow keys are used
|
||||||
|
if (listView && listView.enableAutoScroll) {
|
||||||
|
listView.enableAutoScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0)
|
||||||
|
updateSelectedIdFromIndex()
|
||||||
|
selectionVersion++
|
||||||
|
ensureVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleGroupExpanded() {
|
||||||
|
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
|
||||||
|
return
|
||||||
|
|
||||||
|
const currentItem = flatNavigation[selectedFlatIndex]
|
||||||
|
const groups = NotificationService.groupedNotifications
|
||||||
|
const group = groups[currentItem.groupIndex]
|
||||||
|
if (!group)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Prevent expanding groups with < 2 notifications
|
||||||
|
const notificationCount = group.notifications ? group.notifications.length : 0
|
||||||
|
if (notificationCount < 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
const wasExpanded = NotificationService.expandedGroups[group.key] || false
|
||||||
|
const groupIndex = currentItem.groupIndex
|
||||||
|
|
||||||
|
isTogglingGroup = true
|
||||||
|
NotificationService.toggleGroupExpansion(group.key)
|
||||||
|
rebuildFlatNavigation()
|
||||||
|
|
||||||
|
// Smart selection after toggle
|
||||||
|
if (!wasExpanded) {
|
||||||
|
// Just expanded - move to first notification in the group
|
||||||
|
for (let i = 0; i < flatNavigation.length; i++) {
|
||||||
|
if (flatNavigation[i].type === "notification" && flatNavigation[i].groupIndex === groupIndex) {
|
||||||
|
selectedFlatIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Just collapsed - stay on the group header
|
||||||
|
for (let i = 0; i < flatNavigation.length; i++) {
|
||||||
|
if (flatNavigation[i].type === "group" && flatNavigation[i].groupIndex === groupIndex) {
|
||||||
|
selectedFlatIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isTogglingGroup = false
|
||||||
|
ensureVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEnterKey() {
|
||||||
|
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
|
||||||
|
return
|
||||||
|
|
||||||
|
const currentItem = flatNavigation[selectedFlatIndex]
|
||||||
|
const groups = NotificationService.groupedNotifications
|
||||||
|
const group = groups[currentItem.groupIndex]
|
||||||
|
if (!group)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (currentItem.type === "group") {
|
||||||
|
const notificationCount = group.notifications ? group.notifications.length : 0
|
||||||
|
if (notificationCount >= 2) {
|
||||||
|
toggleGroupExpanded()
|
||||||
|
} else {
|
||||||
|
executeAction(0)
|
||||||
|
}
|
||||||
|
} else if (currentItem.type === "notification") {
|
||||||
|
executeAction(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTextExpanded() {
|
||||||
|
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
|
||||||
|
return
|
||||||
|
|
||||||
|
const currentItem = flatNavigation[selectedFlatIndex]
|
||||||
|
const groups = NotificationService.groupedNotifications
|
||||||
|
const group = groups[currentItem.groupIndex]
|
||||||
|
if (!group)
|
||||||
|
return
|
||||||
|
|
||||||
|
let messageId = ""
|
||||||
|
|
||||||
|
if (currentItem.type === "group") {
|
||||||
|
messageId = group.latestNotification?.notification?.id + "_desc"
|
||||||
|
} else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) {
|
||||||
|
messageId = group.notifications[currentItem.notificationIndex]?.notification?.id + "_desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageId) {
|
||||||
|
NotificationService.toggleMessageExpansion(messageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeAction(actionIndex) {
|
||||||
|
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
|
||||||
|
return
|
||||||
|
|
||||||
|
const currentItem = flatNavigation[selectedFlatIndex]
|
||||||
|
const groups = NotificationService.groupedNotifications
|
||||||
|
const group = groups[currentItem.groupIndex]
|
||||||
|
if (!group)
|
||||||
|
return
|
||||||
|
|
||||||
|
let actions = []
|
||||||
|
|
||||||
|
if (currentItem.type === "group") {
|
||||||
|
actions = group.latestNotification?.actions || []
|
||||||
|
} else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) {
|
||||||
|
actions = group.notifications[currentItem.notificationIndex]?.actions || []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionIndex >= 0 && actionIndex < actions.length) {
|
||||||
|
const action = actions[actionIndex]
|
||||||
|
if (action.invoke) {
|
||||||
|
action.invoke()
|
||||||
|
if (onClose)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelected() {
|
||||||
|
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
|
||||||
|
return
|
||||||
|
|
||||||
|
const currentItem = flatNavigation[selectedFlatIndex]
|
||||||
|
const groups = NotificationService.groupedNotifications
|
||||||
|
const group = groups[currentItem.groupIndex]
|
||||||
|
if (!group)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (currentItem.type === "group") {
|
||||||
|
NotificationService.dismissGroup(group.key)
|
||||||
|
} else if (currentItem.type === "notification") {
|
||||||
|
const notification = group.notifications[currentItem.notificationIndex]
|
||||||
|
NotificationService.dismissNotification(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildFlatNavigation()
|
||||||
|
|
||||||
|
if (flatNavigation.length === 0) {
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
if (listView) {
|
||||||
|
listView.keyboardActive = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1)
|
||||||
|
updateSelectedIdFromIndex()
|
||||||
|
ensureVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureVisible() {
|
||||||
|
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length || !listView)
|
||||||
|
return
|
||||||
|
|
||||||
|
const currentItem = flatNavigation[selectedFlatIndex]
|
||||||
|
|
||||||
|
if (keyboardNavigationActive && currentItem && currentItem.groupIndex >= 0) {
|
||||||
|
// Always center the selected item for better visibility
|
||||||
|
// This ensures the selected item stays in view even when new notifications arrive
|
||||||
|
if (currentItem.type === "notification") {
|
||||||
|
// For individual notifications, center on the group but bias towards the notification
|
||||||
|
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Center)
|
||||||
|
} else {
|
||||||
|
// For group headers, center on the group
|
||||||
|
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force immediate update
|
||||||
|
listView.forceLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKey(event) {
|
||||||
|
if ((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) && (event.modifiers & Qt.ShiftModifier)) {
|
||||||
|
NotificationService.clearAllNotifications()
|
||||||
|
rebuildFlatNavigation()
|
||||||
|
if (flatNavigation.length === 0) {
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
if (listView) {
|
||||||
|
listView.keyboardActive = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedFlatIndex = 0
|
||||||
|
updateSelectedIdFromIndex()
|
||||||
|
}
|
||||||
|
selectionVersion++
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (keyboardNavigationActive) {
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
event.accepted = true
|
||||||
|
} else {
|
||||||
|
if (onClose)
|
||||||
|
onClose()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
} else if (event.key === Qt.Key_Down || event.key === 16777237) {
|
||||||
|
if (!keyboardNavigationActive) {
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
rebuildFlatNavigation() // Ensure we have fresh navigation data
|
||||||
|
selectedFlatIndex = 0
|
||||||
|
updateSelectedIdFromIndex()
|
||||||
|
// Set keyboardActive on listView to show highlight
|
||||||
|
if (listView) {
|
||||||
|
listView.keyboardActive = true
|
||||||
|
}
|
||||||
|
selectionVersion++
|
||||||
|
ensureVisible()
|
||||||
|
event.accepted = true
|
||||||
|
} else {
|
||||||
|
selectNext()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
} else if (event.key === Qt.Key_Up || event.key === 16777235) {
|
||||||
|
if (!keyboardNavigationActive) {
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
rebuildFlatNavigation() // Ensure we have fresh navigation data
|
||||||
|
selectedFlatIndex = 0
|
||||||
|
updateSelectedIdFromIndex()
|
||||||
|
// Set keyboardActive on listView to show highlight
|
||||||
|
if (listView) {
|
||||||
|
listView.keyboardActive = true
|
||||||
|
}
|
||||||
|
selectionVersion++
|
||||||
|
ensureVisible()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (selectedFlatIndex === 0) {
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
// Reset keyboardActive when navigation is disabled
|
||||||
|
if (listView) {
|
||||||
|
listView.keyboardActive = false
|
||||||
|
}
|
||||||
|
selectionVersion++
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
selectPrevious()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
} else if (keyboardNavigationActive) {
|
||||||
|
if (event.key === Qt.Key_Space) {
|
||||||
|
toggleGroupExpanded()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
handleEnterKey()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_E) {
|
||||||
|
toggleTextExpanded()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) {
|
||||||
|
clearSelected()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Tab) {
|
||||||
|
selectNextWrapping()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) {
|
||||||
|
const actionIndex = event.key - Qt.Key_1
|
||||||
|
executeAction(actionIndex)
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_F10) {
|
||||||
|
showKeyboardHints = !showKeyboardHints
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current selection info for UI
|
||||||
|
function getCurrentSelection() {
|
||||||
|
if (!keyboardNavigationActive || selectedFlatIndex < 0 || selectedFlatIndex >= flatNavigation.length) {
|
||||||
|
return {
|
||||||
|
"type": "",
|
||||||
|
"groupIndex": -1,
|
||||||
|
"notificationIndex": -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = flatNavigation[selectedFlatIndex] || {
|
||||||
|
"type": "",
|
||||||
|
"groupIndex": -1,
|
||||||
|
"notificationIndex": -1
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool showHints: false
|
||||||
|
|
||||||
|
height: 80
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
||||||
|
border.color: Theme.primary
|
||||||
|
border.width: 2
|
||||||
|
opacity: showHints ? 1 : 0
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "↑/↓: Nav • Space: Expand • Enter: Action/Expand • E: Text"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Del: Clear • Shift+Del: Clear All • 1-9: Actions • F10: Help • Esc: Close"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool expanded: false
|
||||||
|
readonly property real contentHeight: contentColumn.height + Theme.spacingL * 2
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: expanded ? Math.min(contentHeight, 400) : 0
|
||||||
|
visible: expanded
|
||||||
|
clip: true
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Anims.durShort
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Anims.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opacity: expanded ? 1 : 0
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Anims.durShort
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Anims.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var timeoutOptions: [{
|
||||||
|
"text": "Never",
|
||||||
|
"value": 0
|
||||||
|
}, {
|
||||||
|
"text": "1 second",
|
||||||
|
"value": 1000
|
||||||
|
}, {
|
||||||
|
"text": "3 seconds",
|
||||||
|
"value": 3000
|
||||||
|
}, {
|
||||||
|
"text": "5 seconds",
|
||||||
|
"value": 5000
|
||||||
|
}, {
|
||||||
|
"text": "8 seconds",
|
||||||
|
"value": 8000
|
||||||
|
}, {
|
||||||
|
"text": "10 seconds",
|
||||||
|
"value": 10000
|
||||||
|
}, {
|
||||||
|
"text": "15 seconds",
|
||||||
|
"value": 15000
|
||||||
|
}, {
|
||||||
|
"text": "30 seconds",
|
||||||
|
"value": 30000
|
||||||
|
}, {
|
||||||
|
"text": "1 minute",
|
||||||
|
"value": 60000
|
||||||
|
}, {
|
||||||
|
"text": "2 minutes",
|
||||||
|
"value": 120000
|
||||||
|
}, {
|
||||||
|
"text": "5 minutes",
|
||||||
|
"value": 300000
|
||||||
|
}, {
|
||||||
|
"text": "10 minutes",
|
||||||
|
"value": 600000
|
||||||
|
}]
|
||||||
|
|
||||||
|
function getTimeoutText(value) {
|
||||||
|
if (value === undefined || value === null || isNaN(value)) {
|
||||||
|
return "5 seconds"
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||||
|
if (timeoutOptions[i].value === value) {
|
||||||
|
return timeoutOptions[i].text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value === 0) {
|
||||||
|
return "Never"
|
||||||
|
}
|
||||||
|
if (value < 1000) {
|
||||||
|
return value + "ms"
|
||||||
|
}
|
||||||
|
if (value < 60000) {
|
||||||
|
return Math.round(value / 1000) + " seconds"
|
||||||
|
}
|
||||||
|
return Math.round(value / 60000) + " minutes"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: contentColumn
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Notification Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 36
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Do Not Disturb"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
checked: SessionData.doNotDisturb
|
||||||
|
onToggled: SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Notification Timeouts"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
width: parent.width
|
||||||
|
text: "Low Priority"
|
||||||
|
description: "Timeout for low priority notifications"
|
||||||
|
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
|
||||||
|
options: timeoutOptions.map(opt => opt.text)
|
||||||
|
onValueChanged: value => {
|
||||||
|
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||||
|
if (timeoutOptions[i].text === value) {
|
||||||
|
SettingsData.setNotificationTimeoutLow(timeoutOptions[i].value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
width: parent.width
|
||||||
|
text: "Normal Priority"
|
||||||
|
description: "Timeout for normal priority notifications"
|
||||||
|
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
|
||||||
|
options: timeoutOptions.map(opt => opt.text)
|
||||||
|
onValueChanged: value => {
|
||||||
|
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||||
|
if (timeoutOptions[i].text === value) {
|
||||||
|
SettingsData.setNotificationTimeoutNormal(timeoutOptions[i].value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
width: parent.width
|
||||||
|
text: "Critical Priority"
|
||||||
|
description: "Timeout for critical priority notifications"
|
||||||
|
currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical)
|
||||||
|
options: timeoutOptions.map(opt => opt.text)
|
||||||
|
onValueChanged: value => {
|
||||||
|
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||||
|
if (timeoutOptions[i].text === value) {
|
||||||
|
SettingsData.setNotificationTimeoutCritical(timeoutOptions[i].value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 36
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "notifications_active"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: SettingsData.notificationOverlayEnabled ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Notification Overlay"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Display all priorities over fullscreen apps"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
checked: SettingsData.notificationOverlayEnabled
|
||||||
|
onToggled: toggled => SettingsData.setNotificationOverlayEnabled(toggled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,586 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: win
|
||||||
|
|
||||||
|
required property var notificationData
|
||||||
|
required property string notificationId
|
||||||
|
readonly property bool hasValidData: notificationData && notificationData.notification
|
||||||
|
property int screenY: 0
|
||||||
|
property bool exiting: false
|
||||||
|
property bool _isDestroying: false
|
||||||
|
property bool _finalized: false
|
||||||
|
|
||||||
|
signal entered
|
||||||
|
signal exitFinished
|
||||||
|
|
||||||
|
function startExit() {
|
||||||
|
if (exiting || _isDestroying) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exiting = true
|
||||||
|
exitAnim.restart()
|
||||||
|
exitWatchdog.restart()
|
||||||
|
if (NotificationService.removeFromVisibleNotifications)
|
||||||
|
NotificationService.removeFromVisibleNotifications(win.notificationData)
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceExit() {
|
||||||
|
if (_isDestroying) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_isDestroying = true
|
||||||
|
exiting = true
|
||||||
|
visible = false
|
||||||
|
exitWatchdog.stop()
|
||||||
|
finalizeExit("forced")
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeExit(reason) {
|
||||||
|
if (_finalized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_finalized = true
|
||||||
|
_isDestroying = true
|
||||||
|
exitWatchdog.stop()
|
||||||
|
wrapperConn.enabled = false
|
||||||
|
wrapperConn.target = null
|
||||||
|
win.exitFinished()
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: hasValidData
|
||||||
|
WlrLayershell.layer: {
|
||||||
|
if (!notificationData)
|
||||||
|
return WlrLayershell.Top
|
||||||
|
|
||||||
|
SettingsData.notificationOverlayEnabled
|
||||||
|
|
||||||
|
const shouldUseOverlay = (SettingsData.notificationOverlayEnabled) || (notificationData.urgency === NotificationUrgency.Critical)
|
||||||
|
|
||||||
|
return shouldUseOverlay ? WlrLayershell.Overlay : WlrLayershell.Top
|
||||||
|
}
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
color: "transparent"
|
||||||
|
implicitWidth: 400
|
||||||
|
implicitHeight: 122
|
||||||
|
onScreenYChanged: margins.top = Theme.barHeight - 4 + SettingsData.topBarSpacing + 4 + screenY
|
||||||
|
onHasValidDataChanged: {
|
||||||
|
if (!hasValidData && !exiting && !_isDestroying) {
|
||||||
|
forceExit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (hasValidData) {
|
||||||
|
Qt.callLater(() => enterX.restart())
|
||||||
|
} else {
|
||||||
|
forceExit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onNotificationDataChanged: {
|
||||||
|
if (!_isDestroying) {
|
||||||
|
wrapperConn.target = win.notificationData || null
|
||||||
|
notificationConn.target = (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onEntered: {
|
||||||
|
if (!_isDestroying) {
|
||||||
|
enterDelay.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component.onDestruction: {
|
||||||
|
_isDestroying = true
|
||||||
|
exitWatchdog.stop()
|
||||||
|
if (notificationData && notificationData.timer) {
|
||||||
|
notificationData.timer.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
|
||||||
|
margins {
|
||||||
|
top: Theme.barHeight - 4 + SettingsData.topBarSpacing + 4
|
||||||
|
right: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: win.hasValidData
|
||||||
|
layer.enabled: (enterX.running || exitAnim.running)
|
||||||
|
layer.smooth: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property var shadowLayers: [shadowLayer1, shadowLayer2, shadowLayer3]
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 4
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: shadowLayer1
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -3
|
||||||
|
color: "transparent"
|
||||||
|
radius: parent.radius + 3
|
||||||
|
border.color: Qt.rgba(0, 0, 0, 0.05)
|
||||||
|
border.width: 1
|
||||||
|
z: -3
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: shadowLayer2
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -2
|
||||||
|
color: "transparent"
|
||||||
|
radius: parent.radius + 2
|
||||||
|
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
z: -2
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: shadowLayer3
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
radius: parent.radius
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: parent.radius
|
||||||
|
visible: notificationData && notificationData.urgency === NotificationUrgency.Critical
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Horizontal
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
position: 0
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
position: 0.02
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
position: 0.021
|
||||||
|
color: "transparent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: notificationContent
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: 12
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.rightMargin: 56
|
||||||
|
height: 98
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: iconContainer
|
||||||
|
|
||||||
|
readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== ""
|
||||||
|
|
||||||
|
width: 55
|
||||||
|
height: 55
|
||||||
|
radius: 27.5
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||||
|
border.color: "transparent"
|
||||||
|
border.width: 0
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: iconImage
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
asynchronous: true
|
||||||
|
source: {
|
||||||
|
if (!notificationData)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if (parent.hasNotificationImage)
|
||||||
|
return notificationData.cleanImage || ""
|
||||||
|
|
||||||
|
if (notificationData.appIcon) {
|
||||||
|
const appIcon = notificationData.appIcon
|
||||||
|
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
|
||||||
|
return appIcon
|
||||||
|
|
||||||
|
return Quickshell.iconPath(appIcon, true)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !parent.hasNotificationImage && (!notificationData || !notificationData.appIcon || notificationData.appIcon === "")
|
||||||
|
text: {
|
||||||
|
const appName = notificationData && notificationData.appName ? notificationData.appName : "?"
|
||||||
|
return appName.charAt(0).toUpperCase()
|
||||||
|
}
|
||||||
|
font.pixelSize: 20
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.primaryText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: textContainer
|
||||||
|
|
||||||
|
anchors.left: iconContainer.right
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 0
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: -2
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: {
|
||||||
|
if (!notificationData)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
const appName = notificationData.appName || ""
|
||||||
|
const timeStr = notificationData.timeStr || ""
|
||||||
|
if (timeStr.length > 0)
|
||||||
|
return appName + " • " + timeStr
|
||||||
|
else
|
||||||
|
return appName
|
||||||
|
}
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: notificationData ? (notificationData.summary || "") : ""
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: notificationData ? (notificationData.htmlBody || "") : ""
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
visible: text.length > 0
|
||||||
|
linkColor: Theme.primary
|
||||||
|
onLinkActivated: link => {
|
||||||
|
return Qt.openUrlExternally(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: closeButton
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 12
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: 18
|
||||||
|
buttonSize: 28
|
||||||
|
z: 15
|
||||||
|
onClicked: {
|
||||||
|
if (notificationData && !win.exiting)
|
||||||
|
notificationData.popup = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: clearButton.left
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
spacing: 8
|
||||||
|
z: 20
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: notificationData ? (notificationData.actions || []) : []
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isHovered: false
|
||||||
|
|
||||||
|
width: Math.max(actionText.implicitWidth + 12, 50)
|
||||||
|
height: 24
|
||||||
|
radius: 4
|
||||||
|
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: actionText
|
||||||
|
|
||||||
|
text: modelData.text || "View"
|
||||||
|
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onEntered: parent.isHovered = true
|
||||||
|
onExited: parent.isHovered = false
|
||||||
|
onClicked: {
|
||||||
|
if (modelData && modelData.invoke)
|
||||||
|
modelData.invoke()
|
||||||
|
|
||||||
|
if (notificationData && !win.exiting)
|
||||||
|
notificationData.popup = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: clearButton
|
||||||
|
|
||||||
|
property bool isHovered: false
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
width: Math.max(clearText.implicitWidth + 12, 50)
|
||||||
|
height: 24
|
||||||
|
radius: 4
|
||||||
|
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||||
|
z: 20
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: clearText
|
||||||
|
|
||||||
|
text: "Clear"
|
||||||
|
color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onEntered: clearButton.isHovered = true
|
||||||
|
onExited: clearButton.isHovered = false
|
||||||
|
onClicked: {
|
||||||
|
if (notificationData && !win.exiting)
|
||||||
|
NotificationService.dismissNotification(notificationData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cardHoverArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
propagateComposedEvents: true
|
||||||
|
z: -1
|
||||||
|
onEntered: {
|
||||||
|
if (notificationData && notificationData.timer)
|
||||||
|
notificationData.timer.stop()
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
if (notificationData && notificationData.popup && notificationData.timer)
|
||||||
|
notificationData.timer.restart()
|
||||||
|
}
|
||||||
|
onClicked: {
|
||||||
|
if (notificationData && !win.exiting)
|
||||||
|
notificationData.popup = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transform: Translate {
|
||||||
|
id: tx
|
||||||
|
|
||||||
|
x: Anims.slidePx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
id: enterX
|
||||||
|
|
||||||
|
target: tx
|
||||||
|
property: "x"
|
||||||
|
from: Anims.slidePx
|
||||||
|
to: 0
|
||||||
|
duration: Anims.durMed
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Anims.emphasizedDecel
|
||||||
|
onStopped: {
|
||||||
|
if (!win.exiting && !win._isDestroying && Math.abs(tx.x) < 0.5) {
|
||||||
|
win.entered()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: exitAnim
|
||||||
|
|
||||||
|
onStopped: finalizeExit("animStopped")
|
||||||
|
|
||||||
|
PropertyAnimation {
|
||||||
|
target: tx
|
||||||
|
property: "x"
|
||||||
|
from: 0
|
||||||
|
to: Anims.slidePx
|
||||||
|
duration: Anims.durShort
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Anims.emphasizedAccel
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "opacity"
|
||||||
|
from: 1
|
||||||
|
to: 0
|
||||||
|
duration: Anims.durShort
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Anims.standardAccel
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "scale"
|
||||||
|
from: 1
|
||||||
|
to: 0.98
|
||||||
|
duration: Anims.durShort
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Anims.emphasizedAccel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
id: wrapperConn
|
||||||
|
|
||||||
|
function onPopupChanged() {
|
||||||
|
if (!win.notificationData || win._isDestroying)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (!win.notificationData.popup && !win.exiting)
|
||||||
|
startExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
target: win.notificationData || null
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
enabled: !win._isDestroying
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
id: notificationConn
|
||||||
|
|
||||||
|
function onDropped() {
|
||||||
|
if (!win._isDestroying && !win.exiting)
|
||||||
|
forceExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
target: (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
enabled: !win._isDestroying
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: enterDelay
|
||||||
|
|
||||||
|
interval: 160
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
if (notificationData && notificationData.timer && !exiting && !_isDestroying)
|
||||||
|
notificationData.timer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: exitWatchdog
|
||||||
|
|
||||||
|
interval: 600
|
||||||
|
repeat: false
|
||||||
|
onTriggered: finalizeExit("watchdog")
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on screenY {
|
||||||
|
id: screenYAnim
|
||||||
|
|
||||||
|
enabled: !exiting && !_isDestroying
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Anims.durShort
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Anims.standardDecel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: manager
|
||||||
|
|
||||||
|
property var modelData
|
||||||
|
property int topMargin: 0
|
||||||
|
property int baseNotificationHeight: 120
|
||||||
|
property int maxTargetNotifications: 4
|
||||||
|
property var popupWindows: [] // strong refs to windows (live until exitFinished)
|
||||||
|
property var destroyingWindows: new Set()
|
||||||
|
property Component popupComponent
|
||||||
|
|
||||||
|
popupComponent: Component {
|
||||||
|
NotificationPopup {
|
||||||
|
onEntered: manager._onPopupEntered(this)
|
||||||
|
onExitFinished: manager._onPopupExitFinished(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property Connections notificationConnections
|
||||||
|
|
||||||
|
notificationConnections: Connections {
|
||||||
|
function onVisibleNotificationsChanged() {
|
||||||
|
manager._sync(NotificationService.visibleNotifications)
|
||||||
|
}
|
||||||
|
|
||||||
|
target: NotificationService
|
||||||
|
}
|
||||||
|
|
||||||
|
property Timer sweeper
|
||||||
|
|
||||||
|
sweeper: Timer {
|
||||||
|
interval: 500
|
||||||
|
running: false
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
const toRemove = []
|
||||||
|
for (const p of popupWindows) {
|
||||||
|
if (!p) {
|
||||||
|
toRemove.push(p)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const isZombie = p.status === Component.Null || (!p.visible && !p.exiting) || (!p.notificationData && !p._isDestroying) || (!p.hasValidData && !p._isDestroying)
|
||||||
|
if (isZombie) {
|
||||||
|
toRemove.push(p)
|
||||||
|
if (p.forceExit) {
|
||||||
|
p.forceExit()
|
||||||
|
} else if (p.destroy) {
|
||||||
|
try {
|
||||||
|
p.destroy()
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toRemove.length) {
|
||||||
|
popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1)
|
||||||
|
const survivors = _active().sort((a, b) => a.screenY - b.screenY)
|
||||||
|
for (let k = 0; k < survivors.length; ++k) {
|
||||||
|
survivors[k].screenY = topMargin + k * baseNotificationHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (popupWindows.length === 0) {
|
||||||
|
sweeper.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hasWindowFor(w) {
|
||||||
|
return popupWindows.some(p => p && p.notificationData === w && !p._isDestroying && p.status !== Component.Null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isValidWindow(p) {
|
||||||
|
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData
|
||||||
|
}
|
||||||
|
|
||||||
|
function _canMakeRoomFor(wrapper) {
|
||||||
|
const activeWindows = _active()
|
||||||
|
if (activeWindows.length < maxTargetNotifications) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!wrapper || !wrapper.notification) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const incomingUrgency = wrapper.notification.urgency || 0
|
||||||
|
for (const p of activeWindows) {
|
||||||
|
if (!p.notificationData || !p.notificationData.notification) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const existingUrgency = p.notificationData.notification.urgency || 0
|
||||||
|
if (existingUrgency < incomingUrgency) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (existingUrgency === incomingUrgency) {
|
||||||
|
const timer = p.notificationData.timer
|
||||||
|
if (timer && !timer.running) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function _makeRoomForNew(wrapper) {
|
||||||
|
const activeWindows = _active()
|
||||||
|
if (activeWindows.length < maxTargetNotifications) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const toRemove = _selectPopupToRemove(activeWindows, wrapper)
|
||||||
|
if (toRemove && !toRemove.exiting) {
|
||||||
|
toRemove.notificationData.removedByLimit = true
|
||||||
|
toRemove.notificationData.popup = false
|
||||||
|
if (toRemove.notificationData.timer) {
|
||||||
|
toRemove.notificationData.timer.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _selectPopupToRemove(activeWindows, incomingWrapper) {
|
||||||
|
const incomingUrgency = (incomingWrapper && incomingWrapper.notification) ? incomingWrapper.notification.urgency || 0 : 0
|
||||||
|
const sortedWindows = activeWindows.slice().sort((a, b) => {
|
||||||
|
const aUrgency = (a.notificationData && a.notificationData.notification) ? a.notificationData.notification.urgency || 0 : 0
|
||||||
|
const bUrgency = (b.notificationData && b.notificationData.notification) ? b.notificationData.notification.urgency || 0 : 0
|
||||||
|
if (aUrgency !== bUrgency) {
|
||||||
|
return aUrgency - bUrgency
|
||||||
|
}
|
||||||
|
const aTimer = a.notificationData && a.notificationData.timer
|
||||||
|
const bTimer = b.notificationData && b.notificationData.timer
|
||||||
|
const aRunning = aTimer && aTimer.running
|
||||||
|
const bRunning = bTimer && bTimer.running
|
||||||
|
if (aRunning !== bRunning) {
|
||||||
|
return aRunning ? 1 : -1
|
||||||
|
}
|
||||||
|
return b.screenY - a.screenY
|
||||||
|
})
|
||||||
|
return sortedWindows[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function _sync(newWrappers) {
|
||||||
|
for (const w of newWrappers) {
|
||||||
|
if (w && !_hasWindowFor(w)) {
|
||||||
|
insertNewestAtTop(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const p of popupWindows.slice()) {
|
||||||
|
if (!_isValidWindow(p)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1 && !p.exiting) {
|
||||||
|
p.notificationData.removedByLimit = true
|
||||||
|
p.notificationData.popup = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertNewestAtTop(wrapper) {
|
||||||
|
if (!wrapper) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const p of popupWindows) {
|
||||||
|
if (!_isValidWindow(p)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (p.exiting) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.screenY = p.screenY + baseNotificationHeight
|
||||||
|
}
|
||||||
|
const notificationId = wrapper && wrapper.notification ? wrapper.notification.id : ""
|
||||||
|
const win = popupComponent.createObject(null, {
|
||||||
|
"notificationData": wrapper,
|
||||||
|
"notificationId": notificationId,
|
||||||
|
"screenY": topMargin,
|
||||||
|
"screen": manager.modelData
|
||||||
|
})
|
||||||
|
if (!win) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!win.hasValidData) {
|
||||||
|
win.destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
popupWindows.push(win)
|
||||||
|
if (!sweeper.running) {
|
||||||
|
sweeper.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _active() {
|
||||||
|
return popupWindows.filter(p => _isValidWindow(p) && p.notificationData && p.notificationData.popup && !p.exiting)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bottom() {
|
||||||
|
let b = null
|
||||||
|
let maxY = -1
|
||||||
|
for (const p of _active()) {
|
||||||
|
if (p.screenY > maxY) {
|
||||||
|
maxY = p.screenY
|
||||||
|
b = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onPopupEntered(p) {}
|
||||||
|
|
||||||
|
function _onPopupExitFinished(p) {
|
||||||
|
if (!p) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const windowId = p.toString()
|
||||||
|
if (destroyingWindows.has(windowId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
destroyingWindows.add(windowId)
|
||||||
|
const i = popupWindows.indexOf(p)
|
||||||
|
if (i !== -1) {
|
||||||
|
popupWindows.splice(i, 1)
|
||||||
|
popupWindows = popupWindows.slice()
|
||||||
|
}
|
||||||
|
if (NotificationService.releaseWrapper && p.notificationData) {
|
||||||
|
NotificationService.releaseWrapper(p.notificationData)
|
||||||
|
}
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (p && p.destroy) {
|
||||||
|
try {
|
||||||
|
p.destroy()
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Qt.callLater(() => destroyingWindows.delete(windowId))
|
||||||
|
})
|
||||||
|
const survivors = _active().sort((a, b) => a.screenY - b.screenY)
|
||||||
|
for (let k = 0; k < survivors.length; ++k) {
|
||||||
|
survivors[k].screenY = topMargin + k * baseNotificationHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupAllWindows() {
|
||||||
|
sweeper.stop()
|
||||||
|
for (const p of popupWindows.slice()) {
|
||||||
|
if (p) {
|
||||||
|
try {
|
||||||
|
if (p.forceExit) {
|
||||||
|
p.forceExit()
|
||||||
|
} else if (p.destroy) {
|
||||||
|
p.destroy()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupWindows = []
|
||||||
|
destroyingWindows.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
onPopupWindowsChanged: {
|
||||||
|
if (popupWindows.length > 0 && !sweeper.running) {
|
||||||
|
sweeper.start()
|
||||||
|
} else if (popupWindows.length === 0 && sweeper.running) {
|
||||||
|
sweeper.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
quickshell/.config/quickshell/Modules/OSD/BrightnessOSD.qml
Normal file
134
quickshell/.config/quickshell/Modules/OSD/BrightnessOSD.qml
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankOSD {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
|
||||||
|
osdHeight: 40 + Theme.spacingS * 2
|
||||||
|
autoHideInterval: 3000
|
||||||
|
enableMouseInteraction: true
|
||||||
|
|
||||||
|
property var brightnessDebounceTimer: Timer {
|
||||||
|
property int pendingValue: 0
|
||||||
|
|
||||||
|
interval: {
|
||||||
|
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||||
|
return (deviceInfo && deviceInfo.class === "ddc") ? 200 : 50
|
||||||
|
}
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
DisplayService.setBrightnessInternal(pendingValue, DisplayService.lastIpcDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: DisplayService
|
||||||
|
function onBrightnessChanged() {
|
||||||
|
root.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int gap: Theme.spacingS
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize
|
||||||
|
height: Theme.iconSize
|
||||||
|
radius: Theme.iconSize / 2
|
||||||
|
color: "transparent"
|
||||||
|
x: parent.gap
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||||
|
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") {
|
||||||
|
return "brightness_medium"
|
||||||
|
} else if (deviceInfo.name.includes("kbd")) {
|
||||||
|
return "keyboard"
|
||||||
|
} else {
|
||||||
|
return "lightbulb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
id: brightnessSlider
|
||||||
|
|
||||||
|
width: parent.width - Theme.iconSize - parent.gap * 3
|
||||||
|
height: 40
|
||||||
|
x: parent.gap * 2 + Theme.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
minimum: 1
|
||||||
|
maximum: 100
|
||||||
|
enabled: DisplayService.brightnessAvailable
|
||||||
|
showValue: true
|
||||||
|
unit: "%"
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (DisplayService.brightnessAvailable) {
|
||||||
|
value = DisplayService.brightnessLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSliderValueChanged: newValue => {
|
||||||
|
if (DisplayService.brightnessAvailable) {
|
||||||
|
root.brightnessDebounceTimer.pendingValue = newValue
|
||||||
|
root.brightnessDebounceTimer.restart()
|
||||||
|
resetHideTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onContainsMouseChanged: {
|
||||||
|
setChildHovered(containsMouse)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSliderDragFinished: finalValue => {
|
||||||
|
if (DisplayService.brightnessAvailable) {
|
||||||
|
root.brightnessDebounceTimer.stop()
|
||||||
|
DisplayService.setBrightnessInternal(finalValue, DisplayService.lastIpcDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: DisplayService
|
||||||
|
|
||||||
|
function onBrightnessChanged() {
|
||||||
|
if (!brightnessSlider.pressed) {
|
||||||
|
brightnessSlider.value = DisplayService.brightnessLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDeviceSwitched() {
|
||||||
|
if (!brightnessSlider.pressed) {
|
||||||
|
brightnessSlider.value = DisplayService.brightnessLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOsdShown: {
|
||||||
|
if (DisplayService.brightnessAvailable && contentLoader.item) {
|
||||||
|
const slider = contentLoader.item.children[0].children[1]
|
||||||
|
if (slider) {
|
||||||
|
slider.value = DisplayService.brightnessLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankOSD {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
osdWidth: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
osdHeight: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
autoHideInterval: 2000
|
||||||
|
enableMouseInteraction: false
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SessionService
|
||||||
|
function onInhibitorChanged() {
|
||||||
|
root.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: SessionService.idleInhibited ? Theme.primary : Theme.outline
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue