diff --git a/.github/README.md b/.github/README.md index ff34ca061..b0ad4b9c4 100644 --- a/.github/README.md +++ b/.github/README.md @@ -50,7 +50,7 @@ | [Quickshell](https://quickshell.outfoxxed.me/) | A QtQuick-based widget system, used for the status bar, sidebars, etc. | | Others | See [deps-info.md](https://github.com/end-4/dots-hyprland/blob/main/sdata/deps-info.md) | - **Note: No Waybar here. Please stop calling every bar "Waybar".** + _THERE IS NO WAYBAR STOP FUCKING CALLING EVERY BAR WAYBAR_ diff --git a/dots/.config/hypr/custom/env.conf b/dots/.config/hypr/custom/env.conf index fcd59ec86..4e59a32c2 100644 --- a/dots/.config/hypr/custom/env.conf +++ b/dots/.config/hypr/custom/env.conf @@ -1,2 +1,22 @@ # You can put extra environment variables here # https://wiki.hyprland.org/Configuring/Environment-variables/ + +# ######### Input method ########## +# See https://fcitx-im.org/wiki/Using_Fcitx_5_on_Wayland +#env = QT_IM_MODULE, fcitx +#env = XMODIFIERS, @im=fcitx +#env = SDL_IM_MODULE, fcitx +#env = GLFW_IM_MODULE, ibus +#env = INPUT_METHOD, fcitx + +# ######## Wayland ######### +# Tearing +# env = WLR_DRM_NO_ATOMIC, 1 +# ? +# env = WLR_NO_HARDWARE_CURSORS, 1 + +# ######## EDITOR ######### +#https://wiki.archlinux.org/title/Category:Text_editors +# for example: vi nano nvim ... + +#env = EDITOR, vim diff --git a/dots/.config/hypr/custom/execs.conf b/dots/.config/hypr/custom/execs.conf index cae4ef6c5..66bb2b8b5 100644 --- a/dots/.config/hypr/custom/execs.conf +++ b/dots/.config/hypr/custom/execs.conf @@ -1,2 +1,6 @@ # You can make apps auto-start here # Relevant Hyprland wiki section: https://wiki.hyprland.org/Configuring/Keywords/#executing + +# Input method +# exec-once = fcitx5 + diff --git a/dots/.config/hypr/custom/general.conf b/dots/.config/hypr/custom/general.conf index ded706ae7..aa3fd4f83 100644 --- a/dots/.config/hypr/custom/general.conf +++ b/dots/.config/hypr/custom/general.conf @@ -1,2 +1,6 @@ # Put general config stuff here -# Here's a list of every variable: https://wiki.hyprland.org/Configuring/Variables/ \ No newline at end of file +# Here's a list of every variable: https://wiki.hyprland.org/Configuring/Variables/ + +# monitor=,addreserved, 0, 0, 0, 0 # Custom reserved area + +# HDMI port: mirror display. To see device name, use `hyprctl monitors` diff --git a/dots/.config/hypr/custom/keybinds.conf b/dots/.config/hypr/custom/keybinds.conf index f2a568394..8885a2175 100644 --- a/dots/.config/hypr/custom/keybinds.conf +++ b/dots/.config/hypr/custom/keybinds.conf @@ -4,6 +4,19 @@ bind = Ctrl+Super, Slash, exec, xdg-open ~/.config/illogical-impulse/config.json # Edit shell config bind = Ctrl+Super+Alt, Slash, exec, xdg-open ~/.config/hypr/custom/keybinds.conf # Edit extra keybinds +##! Apps +# bind = Super, Return, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "${TERMINAL}" "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # Terminal +# bind = Super, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "${TERMINAL}" "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] (terminal) (alt) +# bind = Ctrl+Alt, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "${TERMINAL}" "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] (terminal) (for Ubuntu people) +# bind = Super, E, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "dolphin" "nautilus" "nemo" "thunar" "${TERMINAL}" "kitty -1 fish -c yazi" # File manager +# bind = Super, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "google-chrome-stable" "zen-browser" "firefox" "brave" "chromium" "microsoft-edge-stable" "opera" "librewolf" # Browser +# bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "cursor" "zed" "zedit" "zeditor" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" "command -v micro && kitty -1 micro" # Code editor +# bind = Ctrl+Super+Shift+Alt, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" "libreoffice" # Office software +# bind = Super, X, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kate" "gnome-text-editor" "emacs" # Text editor +# bind = Ctrl+Super, V, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol" # Volume mixer +# bind = Super, I, exec, XDG_CURRENT_DESKTOP=gnome ~/.config/hypr/hyprland/scripts/launch_first_available.sh "qs -p ~/.config/quickshell/$qsConfig/settings.qml" "systemsettings" "gnome-control-center" "better-control" # Settings app +# bind = Ctrl+Shift, Escape, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "gnome-system-monitor" "plasma-systemmonitor --page-name Processes" "command -v btop && kitty -1 fish -c btop" # Task manager + # Add stuff here # Use #! to add an extra column on the cheatsheet # Use ##! to add a section in that column diff --git a/dots/.config/hypr/custom/rules.conf b/dots/.config/hypr/custom/rules.conf index 159de0915..b253bc8d1 100644 --- a/dots/.config/hypr/custom/rules.conf +++ b/dots/.config/hypr/custom/rules.conf @@ -1,3 +1,11 @@ # You can put custom rules here # Window/layer rules: https://wiki.hyprland.org/Configuring/Window-Rules/ # Workspace rules: https://wiki.hyprland.org/Configuring/Workspace-Rules/ + +# ######## Window rules ######## + +# Uncomment to apply global transparency to all windows: +# windowrulev2 = opacity 0.89 override 0.89 override, class:.* + +# Disable blur for all xwayland apps +# windowrulev2 = noblur, xwayland:1 \ No newline at end of file diff --git a/dots/.config/hypr/hyprland/env.conf b/dots/.config/hypr/hyprland/env.conf index bd45f9f7e..affd1ca82 100644 --- a/dots/.config/hypr/hyprland/env.conf +++ b/dots/.config/hypr/hyprland/env.conf @@ -1,11 +1,3 @@ -# ######### Input method ########## -# See https://fcitx-im.org/wiki/Using_Fcitx_5_on_Wayland -#env = QT_IM_MODULE, fcitx -#env = XMODIFIERS, @im=fcitx -#env = SDL_IM_MODULE, fcitx -#env = GLFW_IM_MODULE, ibus -#env = INPUT_METHOD, fcitx - # ############ Wayland ############# env = ELECTRON_OZONE_PLATFORM_HINT,auto @@ -17,12 +9,6 @@ env = QT_QPA_PLATFORM, wayland env = QT_QPA_PLATFORMTHEME, kde env = XDG_MENU_PREFIX, plasma- -# ######## Wayland ######### -# Tearing -# env = WLR_DRM_NO_ATOMIC, 1 -# ? -# env = WLR_NO_HARDWARE_CURSORS, 1 - # ######## Virtual envrionment ######### env = ILLOGICAL_IMPULSE_VIRTUAL_ENV, ~/.local/state/quickshell/.venv diff --git a/dots/.config/hypr/hyprland/execs.conf b/dots/.config/hypr/hyprland/execs.conf index bb5dd7225..2cad1308e 100644 --- a/dots/.config/hypr/hyprland/execs.conf +++ b/dots/.config/hypr/hyprland/execs.conf @@ -3,9 +3,6 @@ exec-once = ~/.config/hypr/hyprland/scripts/start_geoclue_agent.sh exec-once = qs -c $qsConfig & exec-once = ~/.config/hypr/custom/scripts/__restore_video_wallpaper.sh -# Input method -# exec-once = fcitx5 - # Core components (authentication, lock screen, notification daemon) exec-once = gnome-keyring-daemon --start --components=secrets exec-once = hypridle diff --git a/dots/.config/hypr/hyprland/general.conf b/dots/.config/hypr/hyprland/general.conf index c4c54bc28..145cbb77c 100644 --- a/dots/.config/hypr/hyprland/general.conf +++ b/dots/.config/hypr/hyprland/general.conf @@ -1,15 +1,11 @@ # MONITOR CONFIG monitor=,preferred,auto,1 -# monitor=,addreserved, 0, 0, 0, 0 # Custom reserved area - -# HDMI port: mirror display. To see device name, use `hyprctl monitors` -# monitor=HDMI-A-1,1920x1080@60,1920x0,1,mirror,eDP-1 gesture = 3, swipe, move, gesture = 3, pinch, float gesture = 4, horizontal, workspace -gesture = 4, up, dispatcher, global, quickshell:overviewToggle -gesture = 4, down, dispatcher, global, quickshell:overviewClose +gesture = 4, up, dispatcher, global, quickshell:overviewWorkspacesToggle +gesture = 4, down, dispatcher, global, quickshell:overviewWorkspacesClose gestures { workspace_swipe_distance = 700 workspace_swipe_cancel_ratio = 0.2 @@ -26,7 +22,7 @@ general { gaps_workspaces = 50 border_size = 1 - col.active_border = rgba(0DB7D4FF) + col.active_border = rgba(0DB7D455) col.inactive_border = rgba(31313600) resize_on_border = true @@ -35,10 +31,10 @@ general { allow_tearing = true # This just allows the `immediate` window rule to work snap { - enabled = true - window_gap = 4 - monitor_gap = 5 - respect_gaps = true + enabled = true + window_gap = 4 + monitor_gap = 5 + respect_gaps = true } } @@ -50,6 +46,9 @@ dwindle { } decoration { + # 2 = circle, higher = squircle, 4 = very obvious squircle + # Clear squircles look really off; we use only extra .4 here to make the rounding feel more continuous + rounding_power = 2.4 rounding = 18 blur { @@ -60,10 +59,10 @@ decoration { size = 10 passes = 3 brightness = 1 - noise = 0.15 - contrast = 0.2 - vibrancy = 0.8 - vibrancy_darkness = 0.8 + noise = 0.05 + contrast = 0.89 + vibrancy = 0.5 + vibrancy_darkness = 0.5 popups = false popups_ignorealpha = 0.6 input_methods = true @@ -73,15 +72,15 @@ decoration { shadow { enabled = true ignore_window = true - range = 30 - offset = 0 2 - render_power = 4 - color = rgba(00000010) + range = 50 + offset = 0 4 + render_power = 10 + color = rgba(00000027) } # Dim dim_inactive = true - dim_strength = 0.025 + dim_strength = 0.05 dim_special = 0.07 } @@ -131,7 +130,7 @@ input { natural_scroll = yes disable_while_typing = true clickfinger_behavior = true - scroll_factor = 0.5 + scroll_factor = 0.7 } } diff --git a/dots/.config/hypr/hyprland/keybinds.conf b/dots/.config/hypr/hyprland/keybinds.conf index 020fe582d..0cca263f1 100644 --- a/dots/.config/hypr/hyprland/keybinds.conf +++ b/dots/.config/hypr/hyprland/keybinds.conf @@ -4,27 +4,27 @@ #! ##! Shell # These absolutely need to be on top, or they won't work consistently -bindid = Super, Super_L, Toggle overview, global, quickshell:overviewToggleRelease # Toggle overview/launcher -bindid = Super, Super_R, Toggle overview, global, quickshell:overviewToggleRelease # [hidden] Toggle overview/launcher +bindid = Super, Super_L, Toggle search, global, quickshell:searchToggleRelease # Toggle search +bindid = Super, Super_R, Toggle search, global, quickshell:searchToggleRelease # [hidden] Toggle search bind = Super, Super_L, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || fuzzel # [hidden] Launcher (fallback) bind = Super, Super_R, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || fuzzel # [hidden] Launcher (fallback) -binditn = Super, catchall, global, quickshell:overviewToggleReleaseInterrupt # [hidden] -bind = Ctrl, Super_L, global, quickshell:overviewToggleReleaseInterrupt # [hidden] -bind = Ctrl, Super_R, global, quickshell:overviewToggleReleaseInterrupt # [hidden] -bind = Super, mouse:272, global, quickshell:overviewToggleReleaseInterrupt # [hidden] -bind = Super, mouse:273, global, quickshell:overviewToggleReleaseInterrupt # [hidden] -bind = Super, mouse:274, global, quickshell:overviewToggleReleaseInterrupt # [hidden] -bind = Super, mouse:275, global, quickshell:overviewToggleReleaseInterrupt # [hidden] -bind = Super, mouse:276, global, quickshell:overviewToggleReleaseInterrupt # [hidden] -bind = Super, mouse:277, global, quickshell:overviewToggleReleaseInterrupt # [hidden] -bind = Super, mouse_up, global, quickshell:overviewToggleReleaseInterrupt # [hidden] -bind = Super, mouse_down,global, quickshell:overviewToggleReleaseInterrupt # [hidden] +binditn = Super, catchall, global, quickshell:searchToggleReleaseInterrupt # [hidden] +bind = Ctrl, Super_L, global, quickshell:searchToggleReleaseInterrupt # [hidden] +bind = Ctrl, Super_R, global, quickshell:searchToggleReleaseInterrupt # [hidden] +bind = Super, mouse:272, global, quickshell:searchToggleReleaseInterrupt # [hidden] +bind = Super, mouse:273, global, quickshell:searchToggleReleaseInterrupt # [hidden] +bind = Super, mouse:274, global, quickshell:searchToggleReleaseInterrupt # [hidden] +bind = Super, mouse:275, global, quickshell:searchToggleReleaseInterrupt # [hidden] +bind = Super, mouse:276, global, quickshell:searchToggleReleaseInterrupt # [hidden] +bind = Super, mouse:277, global, quickshell:searchToggleReleaseInterrupt # [hidden] +bind = Super, mouse_up, global, quickshell:searchToggleReleaseInterrupt # [hidden] +bind = Super, mouse_down,global, quickshell:searchToggleReleaseInterrupt # [hidden] bindit = ,Super_L, global, quickshell:workspaceNumber # [hidden] bindit = ,Super_R, global, quickshell:workspaceNumber # [hidden] +bind = Super, Tab, global, quickshell:overviewWorkspacesToggle # Toggle overview bindd = Super, V, Clipboard history >> clipboard, global, quickshell:overviewClipboardToggle # Clipboard history >> clipboard bindd = Super, Period, Emoji >> clipboard, global, quickshell:overviewEmojiToggle # Emoji >> clipboard -bind = Super, Tab, global, quickshell:overviewWorkspacesToggle # [hidden] Toggle overview/launcher (alt) bind = Super, A, global, quickshell:sidebarLeftToggle # Toggle left sidebar bind = Super+Alt, A, global, quickshell:sidebarLeftToggleDetach # [hidden] bind = Super, B, global, quickshell:sidebarLeftToggle # [hidden] @@ -53,7 +53,7 @@ bindd = Ctrl+Super, T, Toggle wallpaper selector, global, quickshell:wallpaperSe bindd = Ctrl+Super+Alt, T, Select random wallpaper, global, quickshell:wallpaperSelectorRandom # Random wallpaper bindd = Ctrl+Super, T, Change wallpaper, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/quickshell/$qsConfig/scripts/colors/switchwall.sh # [hidden] Change wallpaper (fallback) bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $qsConfig & # Restart widgets -bind = Super+Alt, W, global, quickshell:panelFamilyCycle # Cycle panel family +bind = Ctrl+Super, P, global, quickshell:panelFamilyCycle # Cycle panel family ##! Utilities # Screenshot, Record, OCR, Color picker, Clipboard history @@ -218,8 +218,8 @@ submap = global #! # Testing -bind = Super+Alt, f11, exec, bash -c 'RANDOM_IMAGE=$(find ~/Pictures -type f | grep -v -i "nipple" | grep -v -i "pussy" | shuf -n 1); ACTION=$(notify-send "Test notification with body image" "This notification should contain your user account image and Discord icon. Oh and here is a random image in your Pictures folder: \"Testing" -a "Hyprland keybind" -p -h "string:image-path:/var/lib/AccountsService/icons/$USER" -t 6000 -i "discord" -A "openImage=Open profile image" -A "action2=Open the random image" -A "action3=Useless button"); [[ $ACTION == *openImage ]] && xdg-open "/var/lib/AccountsService/icons/$USER"; [[ $ACTION == *action2 ]] && xdg-open \"$RANDOM_IMAGE\"' # [hidden] -bind = Super+Alt, f12, exec, bash -c 'RANDOM_IMAGE=$(find ~/Pictures -type f | grep -v -i "nipple" | grep -v -i "pussy" | shuf -n 1); ACTION=$(notify-send "Test notification" "This notification should contain a random image in your Pictures folder and Discord icon.\nFlick right to dismiss!" -a "Discord (fake)" -p -h "string:image-path:$RANDOM_IMAGE" -t 6000 -i "discord" -A "openImage=Open profile image" -A "action2=Useless button" -A "action3=Cry more"); [[ $ACTION == *openImage ]] && xdg-open "/var/lib/AccountsService/icons/$USER"' # [hidden] +bind = Super+Alt, f11, exec, bash -c 'RANDOM_IMAGE=$(find ~/Pictures -type f | grep -v -i "nipple" | grep -v -i "pussy" | shuf -n 1); ACTION=$(notify-send "Test notification with body image" "This notification should contain your user account image and Discord icon. Oh and here is a random image in your Pictures folder: \"Testing" -a "Hyprland keybind" -p -h "string:image-path:/var/lib/AccountsService/icons/$USER" -t 6000 -i "discord" -A "openImage=Profile image" -A "action2=Open the random image" -A "action3=Useless button"); [[ $ACTION == *openImage ]] && xdg-open "/var/lib/AccountsService/icons/$USER"; [[ $ACTION == *action2 ]] && xdg-open \"$RANDOM_IMAGE\"' # [hidden] +bind = Super+Alt, f12, exec, bash -c 'RANDOM_IMAGE=$(find ~/Pictures -type f | grep -v -i "nipple" | grep -v -i "pussy" | shuf -n 1); ACTION=$(notify-send "Test notification" "This notification should contain a random image in your Pictures folder and Discord icon.\nFlick right to dismiss!" -a "Discord (fake)" -p -h "string:image-path:$RANDOM_IMAGE" -t 6000 -i "discord" -A "openImage=Profile image" -A "action2=Useless button"); [[ $ACTION == *openImage ]] && xdg-open "/var/lib/AccountsService/icons/$USER"' # [hidden] bind = Super+Alt, Equal, exec, notify-send "Urgent notification" "Ah hell no" -u critical -a 'Hyprland keybind' # [hidden] ##! Session diff --git a/dots/.config/hypr/hyprland/rules.conf b/dots/.config/hypr/hyprland/rules.conf index 24df33df8..d97f8d1f5 100644 --- a/dots/.config/hypr/hyprland/rules.conf +++ b/dots/.config/hypr/hyprland/rules.conf @@ -7,8 +7,6 @@ windowrule = no_blur on, match:class ^()$, match:title ^()$ # Disable blur for all xwayland apps # windowrule = no_blur on, match:xwayland 1 -# Disable blur for every window -windowrule = no_blur on, match:class .* # Floating windowrule = center on, match:title ^(Open File)(.*)$ @@ -160,6 +158,9 @@ layerrule = animation slide left, match:namespace quickshell:sidebarLeft layerrule = animation slide, match:namespace quickshell:verticalBar layerrule = animation slide top, match:namespace quickshell:wallpaperSelector layerrule = no_anim on, match:namespace quickshell:wOnScreenDisplay +layerrule = no_anim on, match:namespace quickshell:wStartMenu +layerrule = ignore_alpha 0, quickshell:wTaskView +layerrule = no_anim on, quickshell:wTaskView # Launchers need to be FAST layerrule = no_anim on, match:namespace gtk4-layer-shell diff --git a/dots/.config/matugen/templates/hyprland/colors.conf b/dots/.config/matugen/templates/hyprland/colors.conf index a30e3d4bf..6de98514f 100644 --- a/dots/.config/matugen/templates/hyprland/colors.conf +++ b/dots/.config/matugen/templates/hyprland/colors.conf @@ -1,6 +1,6 @@ general { - col.active_border = rgba({{colors.outline.default.hex_stripped}}AA) - col.inactive_border = rgba({{colors.outline_variant.default.hex_stripped}}AA) + col.active_border = rgba({{colors.outline.default.hex_stripped}}77) + col.inactive_border = rgba({{colors.outline_variant.default.hex_stripped}}55) } misc { diff --git a/dots/.config/quickshell/ii/GlobalStates.qml b/dots/.config/quickshell/ii/GlobalStates.qml index 972495c64..85a0414d6 100644 --- a/dots/.config/quickshell/ii/GlobalStates.qml +++ b/dots/.config/quickshell/ii/GlobalStates.qml @@ -20,6 +20,7 @@ Singleton { property bool overlayOpen: false property bool overviewOpen: false property bool regionSelectorOpen: false + property bool searchOpen: false property bool screenLocked: false property bool screenLockContainsCharacters: false property bool screenUnlockFailed: false diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/.svg b/dots/.config/quickshell/ii/assets/icons/fluent/.svg new file mode 100644 index 000000000..c047f0c03 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/.svg @@ -0,0 +1,35 @@ + + + + + diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/add-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/add-filled.svg new file mode 100644 index 000000000..6b1a81835 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/add-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/add.svg b/dots/.config/quickshell/ii/assets/icons/fluent/add.svg new file mode 100644 index 000000000..c983f3518 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/app-generic-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/app-generic-filled.svg new file mode 100644 index 000000000..87c7b6c41 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/app-generic-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/app-generic.svg b/dots/.config/quickshell/ii/assets/icons/fluent/app-generic.svg new file mode 100644 index 000000000..cd0c7b516 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/app-generic.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/apps-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/apps-filled.svg new file mode 100644 index 000000000..88214527a --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/apps-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/apps.svg b/dots/.config/quickshell/ii/assets/icons/fluent/apps.svg new file mode 100644 index 000000000..5eb188422 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/apps.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/arrow-enter-left-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-enter-left-filled.svg new file mode 100644 index 000000000..f183cc835 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-enter-left-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/arrow-enter-left.svg b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-enter-left.svg new file mode 100644 index 000000000..c9ffd3eda --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-enter-left.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/arrow-right-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-right-filled.svg new file mode 100644 index 000000000..0e4e5daaa --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-right-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/arrow-right.svg b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-right.svg new file mode 100644 index 000000000..db97a6e38 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-right.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/arrow-up-left-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-up-left-filled.svg new file mode 100644 index 000000000..dd5011358 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-up-left-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/arrow-up-left.svg b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-up-left.svg new file mode 100644 index 000000000..8b6683c69 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/arrow-up-left.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/calculator-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/calculator-filled.svg new file mode 100644 index 000000000..92ad06215 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/calculator-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/calculator.svg b/dots/.config/quickshell/ii/assets/icons/fluent/calculator.svg new file mode 100644 index 000000000..0dc9d3056 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/calculator.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/calendar-add-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/calendar-add-filled.svg new file mode 100644 index 000000000..9b12e58e6 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/calendar-add-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/calendar-add.svg b/dots/.config/quickshell/ii/assets/icons/fluent/calendar-add.svg new file mode 100644 index 000000000..fe0ff7263 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/calendar-add.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/camera-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/camera-filled.svg new file mode 100644 index 000000000..643964740 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/camera-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/camera.svg b/dots/.config/quickshell/ii/assets/icons/fluent/camera.svg new file mode 100644 index 000000000..40fa6d1f2 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/checkmark-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark-filled.svg new file mode 100644 index 000000000..966863066 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/checkmark.svg b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark.svg new file mode 100644 index 000000000..07380988b --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/corporation.svg b/dots/.config/quickshell/ii/assets/icons/fluent/corporation.svg new file mode 100644 index 000000000..0ed1a0319 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/corporation.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/crop-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/crop-filled.svg new file mode 100644 index 000000000..f86a4e42c --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/crop-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/crop.svg b/dots/.config/quickshell/ii/assets/icons/fluent/crop.svg new file mode 100644 index 000000000..1447e7c1f --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/crop.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/desktop-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/desktop-filled.svg new file mode 100644 index 000000000..d09661aec --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/desktop-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/desktop.svg b/dots/.config/quickshell/ii/assets/icons/fluent/desktop.svg new file mode 100644 index 000000000..979d35cc3 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/desktop.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/empty.svg b/dots/.config/quickshell/ii/assets/icons/fluent/empty.svg new file mode 100644 index 000000000..c047f0c03 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/empty.svg @@ -0,0 +1,35 @@ + + + + + diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/eye-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/eye-filled.svg new file mode 100644 index 000000000..fe959b74c --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/eye-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/eye-off-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/eye-off-filled.svg new file mode 100644 index 000000000..d3c53b85e --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/eye-off-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/eye-off.svg b/dots/.config/quickshell/ii/assets/icons/fluent/eye-off.svg new file mode 100644 index 000000000..b409256f5 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/eye-off.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/eye.svg b/dots/.config/quickshell/ii/assets/icons/fluent/eye.svg new file mode 100644 index 000000000..55be66814 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/eye.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/globe-search-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/globe-search-filled.svg new file mode 100644 index 000000000..933c6282d --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/globe-search-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/globe-search.svg b/dots/.config/quickshell/ii/assets/icons/fluent/globe-search.svg new file mode 100644 index 000000000..caa2c94ff --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/globe-search.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/image-copy-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/image-copy-filled.svg new file mode 100644 index 000000000..f01cd9872 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/image-copy-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/image-copy.svg b/dots/.config/quickshell/ii/assets/icons/fluent/image-copy.svg new file mode 100644 index 000000000..a40940693 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/image-copy.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/image-edit-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/image-edit-filled.svg new file mode 100644 index 000000000..661466195 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/image-edit-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/image-edit.svg b/dots/.config/quickshell/ii/assets/icons/fluent/image-edit.svg new file mode 100644 index 000000000..6f751b3ab --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/image-edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/image-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/image-filled.svg new file mode 100644 index 000000000..d35954106 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/image-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/image.svg b/dots/.config/quickshell/ii/assets/icons/fluent/image.svg new file mode 100644 index 000000000..379a1cd14 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/image.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/library-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/library-filled.svg new file mode 100644 index 000000000..7afac3d5e --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/library-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/library.svg b/dots/.config/quickshell/ii/assets/icons/fluent/library.svg new file mode 100644 index 000000000..79432ebdd --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/library.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal-filled.svg new file mode 100644 index 000000000..5b5a33c9f --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal.svg b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal.svg new file mode 100644 index 000000000..57e2ee132 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/more-horizontal.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/news-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/news-filled.svg new file mode 100644 index 000000000..1d07cf802 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/news-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/news.svg b/dots/.config/quickshell/ii/assets/icons/fluent/news.svg new file mode 100644 index 000000000..9e0c3a2fe --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/news.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/next-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/next-filled.svg new file mode 100644 index 000000000..d3741963d --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/next-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/next.svg b/dots/.config/quickshell/ii/assets/icons/fluent/next.svg new file mode 100644 index 000000000..26bdcb05e --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/open-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/open-filled.svg new file mode 100644 index 000000000..95ae58d39 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/open-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/open.svg b/dots/.config/quickshell/ii/assets/icons/fluent/open.svg new file mode 100644 index 000000000..561ed5ec2 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/open.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/pause-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/pause-filled.svg new file mode 100644 index 000000000..595435fe7 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/pause-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/pause.svg b/dots/.config/quickshell/ii/assets/icons/fluent/pause.svg new file mode 100644 index 000000000..a334eb60d --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/people-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/people-filled.svg new file mode 100644 index 000000000..96e98b863 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/people-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/people-settings-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/people-settings-filled.svg new file mode 100644 index 000000000..0bcd7d12d --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/people-settings-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/people-settings.svg b/dots/.config/quickshell/ii/assets/icons/fluent/people-settings.svg new file mode 100644 index 000000000..8ff1df32a --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/people-settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/people-team-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/people-team-filled.svg new file mode 100644 index 000000000..84364ec4e --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/people-team-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/people-team.svg b/dots/.config/quickshell/ii/assets/icons/fluent/people-team.svg new file mode 100644 index 000000000..8507e971c --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/people-team.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/people.svg b/dots/.config/quickshell/ii/assets/icons/fluent/people.svg new file mode 100644 index 000000000..967192214 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/people.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/play-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/play-filled.svg new file mode 100644 index 000000000..ae2a12370 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/play-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/play.svg b/dots/.config/quickshell/ii/assets/icons/fluent/play.svg new file mode 100644 index 000000000..777cf8903 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/power-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/power-filled.svg new file mode 100644 index 000000000..2cfa6dba7 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/power-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/power.svg b/dots/.config/quickshell/ii/assets/icons/fluent/power.svg new file mode 100644 index 000000000..5c28fe986 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/power.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/previous-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/previous-filled.svg new file mode 100644 index 000000000..b994af7e1 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/previous-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/previous.svg b/dots/.config/quickshell/ii/assets/icons/fluent/previous.svg new file mode 100644 index 000000000..bb61b7779 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/previous.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/record-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/record-filled.svg new file mode 100644 index 000000000..164d37e87 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/record-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/record.svg b/dots/.config/quickshell/ii/assets/icons/fluent/record.svg new file mode 100644 index 000000000..31e957588 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/record.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/scan-text-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/scan-text-filled.svg new file mode 100644 index 000000000..3a8c786b8 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/scan-text-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/scan-text.svg b/dots/.config/quickshell/ii/assets/icons/fluent/scan-text.svg new file mode 100644 index 000000000..beaf70538 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/scan-text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/search-visual-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/search-visual-filled.svg new file mode 100644 index 000000000..1ab1d25b0 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/search-visual-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/search-visual.svg b/dots/.config/quickshell/ii/assets/icons/fluent/search-visual.svg new file mode 100644 index 000000000..167f228d0 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/search-visual.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/server-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/server-filled.svg new file mode 100644 index 000000000..2b0869653 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/server-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/server.svg b/dots/.config/quickshell/ii/assets/icons/fluent/server.svg new file mode 100644 index 000000000..1b876a172 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/server.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/shield-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/shield-filled.svg new file mode 100644 index 000000000..e639be9bf --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/shield-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/shield-lock-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/shield-lock-filled.svg new file mode 100644 index 000000000..b916cb4aa --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/shield-lock-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/shield-lock.svg b/dots/.config/quickshell/ii/assets/icons/fluent/shield-lock.svg new file mode 100644 index 000000000..af0ed6eaa --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/shield-lock.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/shield.svg b/dots/.config/quickshell/ii/assets/icons/fluent/shield.svg new file mode 100644 index 000000000..cc53bbc8c --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/shield.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/start-here-pressed.svg b/dots/.config/quickshell/ii/assets/icons/fluent/start-here-pressed.svg index e6b950eca..2efc81c45 100644 --- a/dots/.config/quickshell/ii/assets/icons/fluent/start-here-pressed.svg +++ b/dots/.config/quickshell/ii/assets/icons/fluent/start-here-pressed.svg @@ -1,24 +1,120 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/start-here.svg b/dots/.config/quickshell/ii/assets/icons/fluent/start-here.svg index 708d5a71b..9f7b4a177 100644 --- a/dots/.config/quickshell/ii/assets/icons/fluent/start-here.svg +++ b/dots/.config/quickshell/ii/assets/icons/fluent/start-here.svg @@ -1,24 +1,120 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/stop-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/stop-filled.svg new file mode 100644 index 000000000..ef8a91225 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/stop-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/stop.svg b/dots/.config/quickshell/ii/assets/icons/fluent/stop.svg new file mode 100644 index 000000000..26eb3ea04 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/stop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/store-microsoft-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/store-microsoft-filled.svg new file mode 100644 index 000000000..6673448f3 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/store-microsoft-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/store-microsoft.svg b/dots/.config/quickshell/ii/assets/icons/fluent/store-microsoft.svg new file mode 100644 index 000000000..affc6cc6e --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/store-microsoft.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/subtract-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/subtract-filled.svg new file mode 100644 index 000000000..128a7f96b --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/subtract-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/subtract.svg b/dots/.config/quickshell/ii/assets/icons/fluent/subtract.svg new file mode 100644 index 000000000..3e1e2471c --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/subtract.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/system-search-checked-dark.svg b/dots/.config/quickshell/ii/assets/icons/fluent/system-search-checked-dark.svg index af58d933f..84f42c500 100644 --- a/dots/.config/quickshell/ii/assets/icons/fluent/system-search-checked-dark.svg +++ b/dots/.config/quickshell/ii/assets/icons/fluent/system-search-checked-dark.svg @@ -26,8 +26,8 @@ inkscape:zoom="4.65625" inkscape:cx="32" inkscape:cy="32" - inkscape:window-width="1197" - inkscape:window-height="1020" + inkscape:window-width="1595" + inkscape:window-height="664" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" @@ -48,10 +48,10 @@ diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/system-search-checked-light.svg b/dots/.config/quickshell/ii/assets/icons/fluent/system-search-checked-light.svg index 8d0e69fce..76af86e67 100644 --- a/dots/.config/quickshell/ii/assets/icons/fluent/system-search-checked-light.svg +++ b/dots/.config/quickshell/ii/assets/icons/fluent/system-search-checked-light.svg @@ -23,10 +23,10 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="6.5849319" - inkscape:cx="26.95548" + inkscape:cx="27.031411" inkscape:cy="26.423963" - inkscape:window-width="1257" - inkscape:window-height="1020" + inkscape:window-width="1621" + inkscape:window-height="820" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" @@ -35,10 +35,10 @@ id="defs2"> diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/video-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/video-filled.svg new file mode 100644 index 000000000..b3d6843ea --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/video-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/video.svg b/dots/.config/quickshell/ii/assets/icons/fluent/video.svg new file mode 100644 index 000000000..021cbd863 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/wand-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/wand-filled.svg new file mode 100644 index 000000000..fffb70e7f --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/wand-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/wand.svg b/dots/.config/quickshell/ii/assets/icons/fluent/wand.svg new file mode 100644 index 000000000..a5033c274 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/wand.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/window-shield-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/window-shield-filled.svg new file mode 100644 index 000000000..4709d3d1e --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/window-shield-filled.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/window-shield.svg b/dots/.config/quickshell/ii/assets/icons/fluent/window-shield.svg new file mode 100644 index 000000000..023ae50bd --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/window-shield.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/modules/common/Appearance.qml b/dots/.config/quickshell/ii/modules/common/Appearance.qml index d528e6bd5..d64253476 100644 --- a/dots/.config/quickshell/ii/modules/common/Appearance.qml +++ b/dots/.config/quickshell/ii/modules/common/Appearance.qml @@ -28,15 +28,11 @@ Singleton { property real autoBackgroundTransparency: { // y = 0.5768x^2 - 0.759x + 0.2896 let x = wallpaperVibrancy let y = 0.5768 * (x * x) - 0.759 * (x) + 0.2896 - return Math.max(0, Math.min(0.22, y)) - } - property real autoContentTransparency: { // y = -10.1734x^2 + 3.4457x + 0.1872 - let x = autoBackgroundTransparency - let y = -10.1734 * (x * x) + 3.4457 * (x) + 0.1872 - return Math.max(0, Math.min(0.6, y)) + return Math.max(0, Math.min(0.22, y)) - 0.12 * (m3colors.darkmode ? 0 : 1) } + property real autoContentTransparency: 0.9 property real backgroundTransparency: Config?.options.appearance.transparency.enable ? Config?.options.appearance.transparency.automatic ? autoBackgroundTransparency : Config?.options.appearance.transparency.backgroundTransparency : 0 - property real contentTransparency: Config?.options.appearance.transparency.enable ? Config?.options.appearance.transparency.automatic ? autoContentTransparency : Config?.options.appearance.transparency.contentTransparency : 0 + property real contentTransparency: Config?.options.appearance.transparency.automatic ? autoContentTransparency : Config?.options.appearance.transparency.contentTransparency m3colors: QtObject { property bool darkmode: true @@ -114,30 +110,41 @@ Singleton { colors: QtObject { property color colSubtext: m3colors.m3outline - property color colLayer0: ColorUtils.mix(ColorUtils.transparentize(m3colors.m3background, root.backgroundTransparency), m3colors.m3primary, Config.options.appearance.extraBackgroundTint ? 0.99 : 1) + // Layer 0 + property color colLayer0Base: ColorUtils.mix(m3colors.m3background, m3colors.m3primary, Config.options.appearance.extraBackgroundTint ? 0.99 : 1) + property color colLayer0: ColorUtils.transparentize(colLayer0Base, root.backgroundTransparency) property color colOnLayer0: m3colors.m3onBackground property color colLayer0Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.9, root.contentTransparency)) property color colLayer0Active: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.8, root.contentTransparency)) property color colLayer0Border: ColorUtils.mix(root.m3colors.m3outlineVariant, colLayer0, 0.4) - property color colLayer1: ColorUtils.transparentize(m3colors.m3surfaceContainerLow, root.contentTransparency); + // Layer 1 + property color colLayer1Base: m3colors.m3surfaceContainerLow + property color colLayer1: ColorUtils.solveOverlayColor(colLayer0Base, colLayer1Base, 1 - root.contentTransparency); property color colOnLayer1: m3colors.m3onSurfaceVariant; property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45); - property color colLayer2: ColorUtils.transparentize(m3colors.m3surfaceContainer, root.contentTransparency) - property color colOnLayer2: m3colors.m3onSurface; - property color colOnLayer2Disabled: ColorUtils.mix(colOnLayer2, m3colors.m3background, 0.4); property color colLayer1Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.92), root.contentTransparency) property color colLayer1Active: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.85), root.contentTransparency); - property color colLayer2Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.90), root.contentTransparency) - property color colLayer2Active: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.80), root.contentTransparency); - property color colLayer2Disabled: ColorUtils.transparentize(ColorUtils.mix(colLayer2, m3colors.m3background, 0.8), root.contentTransparency); - property color colLayer3: ColorUtils.transparentize(m3colors.m3surfaceContainerHigh, root.contentTransparency) + // Layer 2 + property color colLayer2Base: m3colors.m3surfaceContainer + property color colLayer2: ColorUtils.solveOverlayColor(colLayer1Base, colLayer2Base, 1 - root.contentTransparency) + property color colLayer2Hover: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, colOnLayer2, 0.90), 1 - root.contentTransparency) + property color colLayer2Active: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, colOnLayer2, 0.80), 1 - root.contentTransparency); + property color colLayer2Disabled: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, m3colors.m3background, 0.8), 1 - root.contentTransparency); + property color colOnLayer2: m3colors.m3onSurface; + property color colOnLayer2Disabled: ColorUtils.mix(colOnLayer2, m3colors.m3background, 0.4); + // Layer 3 + property color colLayer3Base: m3colors.m3surfaceContainerHigh + property color colLayer3: ColorUtils.solveOverlayColor(colLayer2Base, colLayer3Base, 1 - root.contentTransparency) + property color colLayer3Hover: ColorUtils.solveOverlayColor(colLayer2Base, ColorUtils.mix(colLayer3Base, colOnLayer3, 0.90), 1 - root.contentTransparency) + property color colLayer3Active: ColorUtils.solveOverlayColor(colLayer2Base, ColorUtils.mix(colLayer3Base, colOnLayer3, 0.80), 1 - root.contentTransparency); property color colOnLayer3: m3colors.m3onSurface; - property color colLayer3Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer3, colOnLayer3, 0.90), root.contentTransparency) - property color colLayer3Active: ColorUtils.transparentize(ColorUtils.mix(colLayer3, colOnLayer3, 0.80), root.contentTransparency); - property color colLayer4: ColorUtils.transparentize(m3colors.m3surfaceContainerHighest, root.contentTransparency) + // Layer 4 + property color colLayer4Base: m3colors.m3surfaceContainerHighest + property color colLayer4: ColorUtils.solveOverlayColor(colLayer3Base, colLayer4Base, 1 - root.contentTransparency) + property color colLayer4Hover: ColorUtils.solveOverlayColor(colLayer3Base, ColorUtils.mix(colLayer4Base, colOnLayer4, 0.90), 1 - root.contentTransparency) + property color colLayer4Active: ColorUtils.solveOverlayColor(colLayer3Base, ColorUtils.mix(colLayer4Base, colOnLayer4, 0.80), 1 - root.contentTransparency); property color colOnLayer4: m3colors.m3onSurface; - property color colLayer4Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer4, colOnLayer4, 0.90), root.contentTransparency) - property color colLayer4Active: ColorUtils.transparentize(ColorUtils.mix(colLayer4, colOnLayer4, 0.80), root.contentTransparency); + // Primary property color colPrimary: m3colors.m3primary property color colOnPrimary: m3colors.m3onPrimary property color colPrimaryHover: ColorUtils.mix(colors.colPrimary, colLayer1Hover, 0.87) @@ -146,13 +153,16 @@ Singleton { property color colPrimaryContainerHover: ColorUtils.mix(colors.colPrimaryContainer, colors.colOnPrimaryContainer, 0.9) property color colPrimaryContainerActive: ColorUtils.mix(colors.colPrimaryContainer, colors.colOnPrimaryContainer, 0.8) property color colOnPrimaryContainer: m3colors.m3onPrimaryContainer + // Secondary property color colSecondary: m3colors.m3secondary - property color colOnSecondary: m3colors.m3onSecondary property color colSecondaryHover: ColorUtils.mix(m3colors.m3secondary, colLayer1Hover, 0.85) property color colSecondaryActive: ColorUtils.mix(m3colors.m3secondary, colLayer1Active, 0.4) + property color colOnSecondary: m3colors.m3onSecondary property color colSecondaryContainer: m3colors.m3secondaryContainer property color colSecondaryContainerHover: ColorUtils.mix(m3colors.m3secondaryContainer, m3colors.m3onSecondaryContainer, 0.90) property color colSecondaryContainerActive: ColorUtils.mix(m3colors.m3secondaryContainer, m3colors.m3onSecondaryContainer, 0.54) + property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer + // Tertiary property color colTertiary: m3colors.m3tertiary property color colTertiaryHover: ColorUtils.mix(m3colors.m3tertiary, colLayer1Hover, 0.85) property color colTertiaryActive: ColorUtils.mix(m3colors.m3tertiary, colLayer1Active, 0.4) @@ -161,16 +171,17 @@ Singleton { property color colTertiaryContainerActive: ColorUtils.mix(m3colors.m3tertiaryContainer, colLayer1Active, 0.54) property color colOnTertiary: m3colors.m3onTertiary property color colOnTertiaryContainer: m3colors.m3onTertiaryContainer - property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer - property color colSurfaceContainerLow: ColorUtils.transparentize(m3colors.m3surfaceContainerLow, root.contentTransparency) - property color colSurfaceContainer: ColorUtils.transparentize(m3colors.m3surfaceContainer, root.contentTransparency) + // Surface property color colBackgroundSurfaceContainer: ColorUtils.transparentize(m3colors.m3surfaceContainer, root.backgroundTransparency) - property color colSurfaceContainerHigh: ColorUtils.transparentize(m3colors.m3surfaceContainerHigh, root.contentTransparency) - property color colSurfaceContainerHighest: ColorUtils.transparentize(m3colors.m3surfaceContainerHighest, root.contentTransparency) + property color colSurfaceContainerLow: ColorUtils.solveOverlayColor(m3colors.m3background, m3colors.m3surfaceContainerLow, 1 - root.contentTransparency) + property color colSurfaceContainer: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainerLow, m3colors.m3surfaceContainer, 1 - root.contentTransparency) + property color colSurfaceContainerHigh: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainer, m3colors.m3surfaceContainerHigh, 1 - root.contentTransparency) + property color colSurfaceContainerHighest: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainerHigh, m3colors.m3surfaceContainerHighest, 1 - root.contentTransparency) property color colSurfaceContainerHighestHover: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.95) property color colSurfaceContainerHighestActive: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.85) property color colOnSurface: m3colors.m3onSurface property color colOnSurfaceVariant: m3colors.m3onSurfaceVariant + // Misc property color colTooltip: m3colors.m3inverseSurface property color colOnTooltip: m3colors.m3inverseOnSurface property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5) @@ -278,6 +289,7 @@ Singleton { property int velocity: 650 property Component numberAnimation: Component { NumberAnimation { + alwaysRunToEnd: true duration: root.animation.elementMoveEnter.duration easing.type: root.animation.elementMoveEnter.type easing.bezierCurve: root.animation.elementMoveEnter.bezierCurve @@ -292,6 +304,7 @@ Singleton { property int velocity: 650 property Component numberAnimation: Component { NumberAnimation { + alwaysRunToEnd: true duration: root.animation.elementMoveExit.duration easing.type: root.animation.elementMoveExit.type easing.bezierCurve: root.animation.elementMoveExit.bezierCurve @@ -310,9 +323,10 @@ Singleton { easing.bezierCurve: root.animation.elementMoveFast.bezierCurve }} property Component numberAnimation: Component { NumberAnimation { - duration: root.animation.elementMoveFast.duration - easing.type: root.animation.elementMoveFast.type - easing.bezierCurve: root.animation.elementMoveFast.bezierCurve + alwaysRunToEnd: true + duration: root.animation.elementMoveFast.duration + easing.type: root.animation.elementMoveFast.type + easing.bezierCurve: root.animation.elementMoveFast.bezierCurve }} } @@ -323,6 +337,7 @@ Singleton { property int velocity: 650 property Component numberAnimation: Component { NumberAnimation { + alwaysRunToEnd: true duration: root.animation.elementResize.duration easing.type: root.animation.elementResize.type easing.bezierCurve: root.animation.elementResize.bezierCurve @@ -336,9 +351,10 @@ Singleton { property list bezierCurve: animationCurves.expressiveDefaultSpatial property int velocity: 850 property Component numberAnimation: Component { NumberAnimation { - duration: root.animation.clickBounce.duration - easing.type: root.animation.clickBounce.type - easing.bezierCurve: root.animation.clickBounce.bezierCurve + alwaysRunToEnd: true + duration: root.animation.clickBounce.duration + easing.type: root.animation.clickBounce.type + easing.bezierCurve: root.animation.clickBounce.bezierCurve }} } diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index 610a4959d..4b4f54f92 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -78,10 +78,7 @@ Singleton { JsonAdapter { id: configOptionsJsonAdapter - property list enabledPanels: [ - "iiBar", "iiBackground", "iiCheatsheet", "iiDock", "iiLock", "iiMediaControls", "iiNotificationPopup", "iiOnScreenDisplay", "iiOnScreenKeyboard", "iiOverlay", "iiOverview", "iiPolkit", "iiRegionSelector", "iiReloadPopup", "iiScreenCorners", "iiSessionScreen", "iiSidebarLeft", "iiSidebarRight", "iiVerticalBar", "iiWallpaperSelector" - ] - property string panelFamily: "ii" // "ii", "w" + property string panelFamily: "ii" // "ii", "waffle" property JsonObject policies: JsonObject { property int ai: 1 // 0: No | 1: Yes | 2: Local @@ -138,6 +135,7 @@ Singleton { } property JsonObject palette: JsonObject { property string type: "auto" // Allowed: auto, scheme-content, scheme-expressive, scheme-fidelity, scheme-fruit-salad, scheme-monochrome, scheme-neutral, scheme-rainbow, scheme-tonal-spot + property string accentColor: "" } } @@ -153,7 +151,9 @@ Singleton { property JsonObject apps: JsonObject { property string bluetooth: "kcmshell6 kcm_bluetooth" - property string network: "kitty -1 fish -c nmtui" + property string changePassword: "kitty -1 --hold=yes fish -i -c 'passwd'" + property string network: "kcmshell6 kcm_networkmanagement" + property string manageUser: "kcmshell6 kcm_users" property string networkEthernet: "kcmshell6 kcm_networkmanagement" property string taskManager: "plasma-systemmonitor --page-name Processes" property string terminal: "kitty -1" // This is only for shell actions @@ -186,7 +186,17 @@ Singleton { property bool useSineCookie: false } property JsonObject digital: JsonObject { + property bool adaptiveAlignment: true + property bool showDate: true property bool animateChange: true + property bool vertical: false + property JsonObject font: JsonObject { + property string family: "Google Sans Flex" + property real weight: 350 + property real width: 100 + property real size: 90 + property real roundness: 0 + } } property JsonObject quote: JsonObject { property bool enable: false @@ -282,6 +292,10 @@ Singleton { property int suspend: 3 } + property JsonObject calendar: JsonObject { + property string locale: "en-GB" + } + property JsonObject cheatsheet: JsonObject { // Use a nerdfont to see the icons // 0: 󰖳 | 1: 󰌽 | 2: 󰘳 | 3:  | 4: 󰨡 @@ -341,6 +355,10 @@ Singleton { } } + property JsonObject launcher: JsonObject { + property list pinnedApps: [ "org.kde.dolphin", "kitty", "cmake-gui"] + } + property JsonObject light: JsonObject { property JsonObject night: JsonObject { property bool automatic: true @@ -407,6 +425,8 @@ Singleton { property real scale: 0.18 // Relative to screen size property real rows: 2 property real columns: 5 + property bool orderRightLeft: false + property bool orderBottomUp: false property bool centerIcons: true } @@ -427,6 +447,9 @@ Singleton { property int strokeWidth: 6 property int padding: 10 } + property JsonObject annotation: JsonObject { + property bool useSatty: false + } } property JsonObject resources: JsonObject { @@ -581,12 +604,13 @@ Singleton { } property JsonObject waffles: JsonObject { - // Animations on Windoes are kinda janky. Setting the following to + // Some spots are kinda janky/awkward. Setting the following to // false will make (some) stuff also be like that for accuracy. // Example: the right-click menu of the Start button property JsonObject tweaks: JsonObject { - property bool smootherMenuAnimations: true property bool switchHandlePositionFix: true + property bool smootherMenuAnimations: true + property bool smootherSearchBar: true } property JsonObject bar: JsonObject { property bool bottom: true @@ -595,6 +619,9 @@ Singleton { property JsonObject actionCenter: JsonObject { property list toggles: [ "network", "bluetooth", "easyEffects", "powerProfile", "idleInhibitor", "nightLight", "darkMode", "antiFlashbang", "cloudflareWarp", "mic", "musicRecognition", "notifications", "onScreenKeyboard", "gameMode", "screenSnip", "colorPicker" ] } + property JsonObject calendar: JsonObject { + property bool force2CharDayOfWeek: true + } } } } diff --git a/dots/.config/quickshell/ii/modules/common/Directories.qml b/dots/.config/quickshell/ii/modules/common/Directories.qml index 56f647684..78b0a9edb 100644 --- a/dots/.config/quickshell/ii/modules/common/Directories.qml +++ b/dots/.config/quickshell/ii/modules/common/Directories.qml @@ -1,6 +1,7 @@ pragma Singleton pragma ComponentBehavior: Bound +import qs.services import qs.modules.common.functions import QtCore import QtQuick @@ -43,9 +44,13 @@ Singleton { property string wallpaperSwitchScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/colors/switchwall.sh`) property string defaultAiPrompts: Quickshell.shellPath("defaults/ai/prompts") property string userAiPrompts: FileUtils.trimFileProtocol(`${Directories.shellConfig}/ai/prompts`) + property string userActions: FileUtils.trimFileProtocol(`${Directories.shellConfig}/actions`) property string aiChats: FileUtils.trimFileProtocol(`${Directories.state}/user/ai/chats`) property string aiTranslationScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/ai/gemini-translate.sh`) property string recordScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/videos/record.sh`) + property string userAvatarPathAccountsService: FileUtils.trimFileProtocol(`/var/lib/AccountsService/icons/${SystemInfo.username}`) + property string userAvatarPathRicersAndWeirdSystems: FileUtils.trimFileProtocol(`${Directories.home}.face`) + property string userAvatarPathRicersAndWeirdSystems2: FileUtils.trimFileProtocol(`${Directories.home}.face.icon`) // Cleanup on init Component.onCompleted: { Quickshell.execDetached(["mkdir", "-p", `${shellConfig}`]) @@ -55,6 +60,7 @@ Singleton { Quickshell.execDetached(["bash", "-c", `rm -rf '${latexOutput}'; mkdir -p '${latexOutput}'`]) Quickshell.execDetached(["bash", "-c", `rm -rf '${cliphistDecode}'; mkdir -p '${cliphistDecode}'`]) Quickshell.execDetached(["mkdir", "-p", `${aiChats}`]) + Quickshell.execDetached(["mkdir", "-p", `${userActions}`]) Quickshell.execDetached(["rm", "-rf", `${tempImages}`]) } } diff --git a/dots/.config/quickshell/ii/modules/common/Persistent.qml b/dots/.config/quickshell/ii/modules/common/Persistent.qml index 478cadd10..8367692e3 100644 --- a/dots/.config/quickshell/ii/modules/common/Persistent.qml +++ b/dots/.config/quickshell/ii/modules/common/Persistent.qml @@ -59,10 +59,14 @@ Singleton { property string hyprlandInstanceSignature: "" property JsonObject ai: JsonObject { - property string model + property string model: "gemini-2.5-flash" property real temperature: 0.5 } + property JsonObject cheatsheet: JsonObject { + property int tabIndex: 0 + } + property JsonObject sidebar: JsonObject { property JsonObject bottomGroup: JsonObject { property bool collapsed: false diff --git a/dots/.config/quickshell/ii/modules/common/functions/ColorUtils.qml b/dots/.config/quickshell/ii/modules/common/functions/ColorUtils.qml index 165f27754..74305c8fa 100644 --- a/dots/.config/quickshell/ii/modules/common/functions/ColorUtils.qml +++ b/dots/.config/quickshell/ii/modules/common/functions/ColorUtils.qml @@ -135,4 +135,38 @@ Singleton { var c = Qt.color(color); return c.hslLightness < 0.5; } + + /** + * Clamps a value to the inclusive range [0, 1]. + * + * @param {number} x - The value to clamp. + * @returns {number} The clamped value in the range [0, 1]. + */ + function clamp01(x) { + return Math.min(1, Math.max(0, x)); + } + + /** + * Solves for the solid overlay color that, when composited over a base color + * with a given opacity, yields the target color. + * + * The compositing equation is: + * result = overlay * overlayOpacity + base * (1 - overlayOpacity) + * + * This function algebraically inverts that equation per channel. + * + * @param {Qt.rgba} baseColor - The base (background) color. + * @param {Qt.rgba} targetColor - The resulting color after compositing. + * @param {number} overlayOpacity - The overlay opacity (0-1). + * @returns {Qt.rgba} The solved overlay color + */ + function solveOverlayColor(baseColor, targetColor, overlayOpacity) { + let invA = 1.0 - overlayOpacity; + + let r = (targetColor.r - baseColor.r * invA) / overlayOpacity; + let g = (targetColor.g - baseColor.g * invA) / overlayOpacity; + let b = (targetColor.b - baseColor.b * invA) / overlayOpacity; + + return Qt.rgba(clamp01(r), clamp01(g), clamp01(b), overlayOpacity); + } } diff --git a/dots/.config/quickshell/ii/modules/common/functions/DateUtils.qml b/dots/.config/quickshell/ii/modules/common/functions/DateUtils.qml new file mode 100644 index 000000000..3cbae5bee --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/functions/DateUtils.qml @@ -0,0 +1,27 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + + function getFirstDayOfWeek(date, firstDay = 1) { + const d = new Date(date); // Copy + const day = d.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday + + // Calculate difference to firstDay + const diff = (day - firstDay + 7) % 7; + d.setDate(d.getDate() - diff); + return d; + } + + function sameDate(d1, d2) { + return (d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate()); + } + + function getIthDayDateOfSameWeek(date, i, firstDay = 1) { + const firstDayDate = root.getFirstDayOfWeek(date, firstDay); + const targetDate = new Date(firstDayDate); + targetDate.setDate(firstDayDate.getDate() + i); + return targetDate; + } +} diff --git a/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml b/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml new file mode 100644 index 000000000..d6adcc0f6 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml @@ -0,0 +1,111 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + /** + * @param { string } summary + * @returns { string } + */ + function findSuitableMaterialSymbol(summary = "") { + const defaultType = 'chat'; + if (summary.length === 0) return defaultType; + + const keywordsToTypes = { + 'reboot': 'restart_alt', + 'record': 'screen_record', + 'battery': 'power', + 'power': 'power', + 'screenshot': 'screenshot_monitor', + 'welcome': 'waving_hand', + 'time': 'scheduleb', + 'installed': 'download', + 'configuration reloaded': 'reset_wrench', + 'unable': 'question_mark', + "couldn't": 'question_mark', + 'config': 'reset_wrench', + 'update': 'update', + 'ai response': 'neurology', + 'control': 'settings', + 'upsca': 'compare', + 'music': 'queue_music', + 'install': 'deployed_code_update', + 'input': 'keyboard_alt', + 'preedit': 'keyboard_alt', + 'startswith:file': 'folder_copy', // Declarative startsWith check + }; + + const lowerSummary = summary.toLowerCase(); + + for (const [keyword, type] of Object.entries(keywordsToTypes)) { + if (keyword.startsWith('startswith:')) { + const startsWithKeyword = keyword.replace('startswith:', ''); + if (lowerSummary.startsWith(startsWithKeyword)) { + return type; + } + } else if (lowerSummary.includes(keyword)) { + return type; + } + } + + return defaultType; + } + + /** + * @param { number | string | Date } timestamp + * @returns { string } + */ + function getFriendlyNotifTimeString(timestamp) { + if (!timestamp) return ''; + const messageTime = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - messageTime.getTime(); + + // Less than 1 minute + if (diffMs < 60000) + return 'Now'; + + // Same day - show relative time + if (messageTime.toDateString() === now.toDateString()) { + const diffMinutes = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + + if (diffHours > 0) { + return `${diffHours}h`; + } else { + return `${diffMinutes}m`; + } + } + + // Yesterday + if (messageTime.toDateString() === new Date(now.getTime() - 86400000).toDateString()) + return 'Yesterday'; + + // Older dates + return Qt.formatDateTime(messageTime, "MMMM dd"); + } + + function processNotificationBody(body, appName) { + let processedBody = body + + // Clean Chromium-based browsers notifications - remove first line + if (appName) { + const lowerApp = appName.toLowerCase() + const chromiumBrowsers = [ + "brave", "chrome", "chromium", "vivaldi", "opera", "microsoft edge" + ] + + if (chromiumBrowsers.some(name => lowerApp.includes(name))) { + const lines = body.split('\n\n') + + if (lines.length > 1 && lines[0].startsWith(' i) +} diff --git a/dots/.config/quickshell/ii/modules/common/models/LauncherSearchResult.qml b/dots/.config/quickshell/ii/modules/common/models/LauncherSearchResult.qml new file mode 100644 index 000000000..c47fc4f39 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/models/LauncherSearchResult.qml @@ -0,0 +1,32 @@ +import QtQuick +import Quickshell + +QtObject { + enum IconType { Material, Text, System, None } + enum FontType { Normal, Monospace } + + // General stuff + property string type: "" + property var fontType: LauncherSearchResult.FontType.Normal + property string name: "" + property string rawValue: "" + property string iconName: "" + property var iconType: LauncherSearchResult.IconType.None + property string verb: "" + property bool blurImage: false + property var execute: () => { + print("Not implemented"); + } + property var actions: [] + + // Stuff needed for DesktopEntry + property string id: "" + property bool shown: true + property string comment: "" + property bool runInTerminal: false + property string genericName: "" + property list keywords: [] + + // Extra stuff to allow for more flexibility + property string category: type +} diff --git a/dots/.config/quickshell/ii/modules/ii/lock/LockContext.qml b/dots/.config/quickshell/ii/modules/common/panels/lock/LockContext.qml similarity index 95% rename from dots/.config/quickshell/ii/modules/ii/lock/LockContext.qml rename to dots/.config/quickshell/ii/modules/common/panels/lock/LockContext.qml index b242cdc05..60d5ac949 100644 --- a/dots/.config/quickshell/ii/modules/ii/lock/LockContext.qml +++ b/dots/.config/quickshell/ii/modules/common/panels/lock/LockContext.qml @@ -21,6 +21,7 @@ Scope { property bool showFailure: false property bool fingerprintsConfigured: false property var targetAction: LockContext.ActionEnum.Unlock + property bool alsoInhibitIdle: false function resetTargetAction() { root.targetAction = LockContext.ActionEnum.Unlock; @@ -58,7 +59,8 @@ Scope { passwordClearTimer.restart(); } - function tryUnlock() { + function tryUnlock(alsoInhibitIdle = false) { + root.alsoInhibitIdle = alsoInhibitIdle; root.unlockInProgress = true; pam.start(); } @@ -70,7 +72,7 @@ Scope { } function stopFingerPam() { - if (fingerPam.running) { + if (fingerPam.active) { fingerPam.abort(); } } diff --git a/dots/.config/quickshell/ii/modules/common/panels/lock/LockScreen.qml b/dots/.config/quickshell/ii/modules/common/panels/lock/LockScreen.qml new file mode 100644 index 000000000..5a1b24d7e --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/panels/lock/LockScreen.qml @@ -0,0 +1,156 @@ +pragma ComponentBehavior: Bound +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +Scope { + id: root + + required property Component lockSurface + property alias context: lockContext + property Component sessionLockSurface: WlSessionLockSurface { + id: sessionLockSurface + color: "transparent" + Loader { + active: GlobalStates.screenLocked + anchors.fill: parent + opacity: active ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + sourceComponent: root.lockSurface + } + } + + Process { + id: unlockKeyringProc + onExited: (exitCode, exitStatus) => { + KeyringStorage.fetchKeyringData(); + } + } + function unlockKeyring() { + unlockKeyringProc.exec({ + environment: ({ + "UNLOCK_PASSWORD": lockContext.currentText + }), + command: ["bash", "-c", Quickshell.shellPath("scripts/keyring/unlock.sh")] + }) + } + + // This stores all the information shared between the lock surfaces on each screen. + // https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen + LockContext { + id: lockContext + + Connections { + target: GlobalStates + function onScreenLockedChanged() { + if (GlobalStates.screenLocked) { + lockContext.reset(); + lockContext.tryFingerUnlock(); + } + } + } + + onUnlocked: (targetAction) => { + // Perform the target action if it's not just unlocking + if (targetAction == LockContext.ActionEnum.Poweroff) { + Session.poweroff(); + return; + } else if (targetAction == LockContext.ActionEnum.Reboot) { + Session.reboot(); + return; + } + + // Unlock the keyring if configured to do so + if (Config.options.lock.security.unlockKeyring) root.unlockKeyring(); // Async + + // Unlock the screen before exiting, or the compositor will display a + // fallback lock you can't interact with. + GlobalStates.screenLocked = false; + + // Refocus last focused window on unlock (hack) + Quickshell.execDetached(["bash", "-c", `sleep 0.2; hyprctl --batch "dispatch togglespecialworkspace; dispatch togglespecialworkspace"`]) + + // Reset + lockContext.reset(); + + // Post-unlock actions + if (lockContext.alsoInhibitIdle) { + lockContext.alsoInhibitIdle = false; + Idle.toggleInhibit(true); + } + } + } + + WlSessionLock { + id: lock + locked: GlobalStates.screenLocked + surface: root.sessionLockSurface + } + + function lock() { + if (Config.options.lock.useHyprlock) { + Quickshell.execDetached(["bash", "-c", "pidof hyprlock || hyprlock"]); + return; + } + GlobalStates.screenLocked = true; + } + + IpcHandler { + target: "lock" + + function activate(): void { + root.lock(); + } + function focus(): void { + lockContext.shouldReFocus(); + } + } + + GlobalShortcut { + name: "lock" + description: "Locks the screen" + + onPressed: { + root.lock() + } + } + + GlobalShortcut { + name: "lockFocus" + description: "Re-focuses the lock screen. This is because Hyprland after waking up for whatever reason" + + "decides to keyboard-unfocus the lock screen" + + onPressed: { + lockContext.shouldReFocus(); + } + } + + function initIfReady() { + if (!Config.ready || !Persistent.ready) return; + if (Config.options.lock.launchOnStartup && Persistent.isNewHyprlandInstance) { + root.lock(); + } else { + KeyringStorage.fetchKeyringData(); + } + } + Connections { + target: Config + function onReadyChanged() { + root.initIfReady(); + } + } + Connections { + target: Persistent + function onReadyChanged() { + root.initIfReady(); + } + } +} diff --git a/dots/.config/quickshell/ii/modules/ii/lock/pam/fprintd.conf b/dots/.config/quickshell/ii/modules/common/panels/lock/pam/fprintd.conf similarity index 100% rename from dots/.config/quickshell/ii/modules/ii/lock/pam/fprintd.conf rename to dots/.config/quickshell/ii/modules/common/panels/lock/pam/fprintd.conf diff --git a/dots/.config/quickshell/ii/modules/common/utils/ImageDownloaderProcess.qml b/dots/.config/quickshell/ii/modules/common/utils/ImageDownloaderProcess.qml index 11ff92a6d..3a86b2fe1 100644 --- a/dots/.config/quickshell/ii/modules/common/utils/ImageDownloaderProcess.qml +++ b/dots/.config/quickshell/ii/modules/common/utils/ImageDownloaderProcess.qml @@ -18,14 +18,19 @@ Process { running: true command: ["bash", "-c", - `mkdir -p $(dirname '${processFilePath(filePath)}'); [ -f '${processFilePath(filePath)}' ] || curl -sSL '${sourceUrl}' -o '${processFilePath(filePath)}' && magick identify -format '%w %h' '${processFilePath(filePath)}'[0]` + `mkdir -p $(dirname '${processFilePath()}'); [ -f '${processFilePath()}' ] || curl -sSL '${sourceUrl}' -o '${processFilePath()}' && file '${processFilePath()}'` ] stdout: StdioCollector { id: imageSizeOutputCollector onStreamFinished: { const output = imageSizeOutputCollector.text.trim(); - const [width, height] = output.split(" ").map(Number); - root.done(root.filePath, width, height); + const match = output.match(/(\d+)\s*x\s*(\d+)/); + + if (match) { + const width = Number(match[1]); + const height = Number(match[2]); + root.done(root.filePath, width, height); + } } } } diff --git a/dots/.config/quickshell/ii/modules/common/utils/ScreenshotAction.qml b/dots/.config/quickshell/ii/modules/common/utils/ScreenshotAction.qml new file mode 100644 index 000000000..831834bd5 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/utils/ScreenshotAction.qml @@ -0,0 +1,81 @@ +pragma ComponentBehavior: Bound +pragma Singleton +import qs.modules.common +import qs.modules.common.utils +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.services +import QtQuick +import QtQuick.Controls +import Qt.labs.synchronizer +import Quickshell + +Singleton { + id: root + + enum Action { + Copy, + Edit, + Search, + CharRecognition, + Record, + RecordWithSound + } + + property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl + property string fileUploadApiEndpoint: "https://uguu.se/upload" + + function getCommand(x, y, width, height, screenshotPath, action, saveDir = "") { + // Set command for action + const rx = Math.round(x); + const ry = Math.round(y); + const rw = Math.round(width); + const rh = Math.round(height); + const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(screenshotPath)} ` + + `-crop ${rw}x${rh}+${rx}+${ry}` + const cropToStdout = `${cropBase} -` + const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'` + const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'` + const slurpRegion = `${rx},${ry} ${rw}x${rh}` + const uploadAndGetUrl = (filePath) => { + return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'` + } + const annotationCommand = `${Config.options.regionSelector.annotation.useSatty ? "satty" : "swappy"} -f -`; + switch (action) { + case ScreenshotAction.Action.Copy: + if (saveDir === "") { + // not saving the screenshot, just copy to clipboard + return ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`] + break; + } + return [ + "bash", "-c", + `mkdir -p '${StringUtils.shellSingleQuoteEscape(saveDir)}' && \ + saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \ + savePath="${saveDir}/$saveFileName" && \ + ${cropToStdout} | tee >(wl-copy) > "$savePath" && \ + ${cleanup}` + ] + + break; + case ScreenshotAction.Action.Edit: + return ["bash", "-c", `${cropToStdout} | ${annotationCommand} && ${cleanup}`] + break; + case ScreenshotAction.Action.Search: + return ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(screenshotPath)})" && ${cleanup}`] + break; + case ScreenshotAction.Action.CharRecognition: + return ["bash", "-c", `${cropInPlace} && tesseract '${StringUtils.shellSingleQuoteEscape(screenshotPath)}' stdout -l $(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/') | wl-copy && ${cleanup}`] + break; + case ScreenshotAction.Action.Record: + return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}'`] + break; + case ScreenshotAction.Action.RecordWithSound: + return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}' --sound`] + break; + default: + console.warn("[Region Selector] Unknown snip action, skipping snip."); + return; + } + } +} diff --git a/dots/.config/quickshell/ii/modules/common/utils/TempScreenshotProcess.qml b/dots/.config/quickshell/ii/modules/common/utils/TempScreenshotProcess.qml new file mode 100644 index 000000000..40c40fb34 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/utils/TempScreenshotProcess.qml @@ -0,0 +1,14 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.modules.common +import qs.modules.common.functions + +Process { + id: screenshotProc + running: true + property string screenshotDir: Directories.screenshotTemp + required property ShellScreen screen + property string screenshotPath: `${screenshotDir}/image-${screen.name}` + command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(screen.name)}' '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`] +} diff --git a/dots/.config/quickshell/ii/modules/common/widgets/CalendarView.qml b/dots/.config/quickshell/ii/modules/common/widgets/CalendarView.qml new file mode 100644 index 000000000..0e1bd13fc --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/CalendarView.qml @@ -0,0 +1,123 @@ +pragma ComponentBehavior: Bound +import QtQml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +Item { + id: root + + // Expose delegate + property Component delegate: Text { + required property var model + text: model.day + } + + // Configuration + property int paddingWeeks: 2 // 1 should be sufficient with proper clipping and no padding + property var locale: Qt.locale() // Should be of type Locale but QML is being funny + + // Scrolling + function scrollMonthsAndSnap(x) { // Scroll x months and snap to month + const focusedDate = root.focusedDate; + const focusedMonth = focusedDate.getMonth(); + const focusedYear = focusedDate.getFullYear(); + const targetMonth = focusedMonth + x; + const targetDate = new Date(focusedYear, targetMonth, 1); + const currentFirstShownDate = new Date(root.dateInFirstWeek.getTime() + (root.paddingWeeks * root.millisPerWeek)); + const diffMillis = targetDate.getTime() - currentFirstShownDate.getTime(); + const diffWeeks = Math.round(diffMillis / root.millisPerWeek); + root.targetWeekDiff += diffWeeks; + } + property int weeksPerScroll: 1 + property real targetWeekDiff: 0 + property real weekDiff: targetWeekDiff + property int contentWeekDiff: weekDiff // whole part of weekDiff + property bool scrolling: false + + Behavior on weekDiff { + id: weekScrollBehavior + animation: Looks.transition.scroll.createObject(this) + } + Timer { + id: scrollAnimationCheckTimer + interval: 30 // Should be plenty for 60fps + onTriggered: root.scrolling = false; + } + onWeekDiffChanged: { + scrolling = true; + scrollAnimationCheckTimer.restart(); + } + + MouseArea { + anchors.fill: parent + onWheel: wheel => { + root.targetWeekDiff += wheel.angleDelta.y / 120 * -root.weeksPerScroll; // Reverse cuz scrolling down should advance + } + } + + // Date calculations + readonly property int millisPerWeek: 7 * 24 * 60 * 60 * 1000 + readonly property int totalWeeks: 6 + (paddingWeeks * 2) + readonly property int focusedWeekIndex: 2 // The third row, 0-indexed + readonly property int focusDayOfWeekIndex: 6 + property date dateInFirstWeek: { + const currentDate = new Date(); + const currentMonth = currentDate.getMonth(); + const currentYear = currentDate.getFullYear(); + const firstDayThisMonth = new Date(currentYear, currentMonth, 1); + return new Date(firstDayThisMonth.getTime() - (paddingWeeks * millisPerWeek) + contentWeekDiff * millisPerWeek); + } + property date focusedDate: { + // The last day of 3rd week shown is considered the focused month + const addedTime = (root.paddingWeeks + root.focusedWeekIndex) * root.millisPerWeek + const dateInTargetWeek = new Date(root.dateInFirstWeek.getTime() + addedTime); + return DateUtils.getIthDayDateOfSameWeek(dateInTargetWeek, root.focusDayOfWeekIndex - root.locale.firstDayOfWeek, root.locale.firstdayOfWeek); // 4 = Thursday + } + property int focusedMonth: focusedDate.getMonth() + 1 // 0-indexed -> 1-indexed + + // Sizes + property real verticalPadding: 0 + property real buttonSize: 40 + property real buttonSpacing: 2 + property real buttonVerticalSpacing: buttonSpacing + implicitHeight: (6 * buttonSize) + (5 * buttonVerticalSpacing) + (2 * verticalPadding) + implicitWidth: weeksColumn.implicitWidth + clip: true + + ColumnLayout { + id: weeksColumn + anchors { + left: parent.left + right: parent.right + } + y: { + const spacePerExtraRow = root.buttonSize + root.buttonVerticalSpacing; + const origin = -(spacePerExtraRow * root.paddingWeeks); + const diff = root.weekDiff * spacePerExtraRow; + return origin + (-diff % spacePerExtraRow) + root.verticalPadding; + } + + spacing: root.buttonVerticalSpacing + + Repeater { + model: root.totalWeeks + + WeekRow { + required property int index + locale: root.locale + date: new Date(root.dateInFirstWeek.getTime() + (index * root.millisPerWeek)) + Layout.fillWidth: true + spacing: root.buttonSpacing + delegate: root.delegate + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/common/widgets/ConfigSlider.qml b/dots/.config/quickshell/ii/modules/common/widgets/ConfigSlider.qml new file mode 100644 index 000000000..edc63191a --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/ConfigSlider.qml @@ -0,0 +1,47 @@ +import qs.modules.common.widgets +import qs.modules.common +import QtQuick +import QtQuick.Layouts +import qs.services + +RowLayout { + id: root + spacing: 10 + Layout.leftMargin: 8 + Layout.rightMargin: 8 + + property string text: "" + property string buttonIcon: "" + property alias value: slider.value + property alias stopIndicatorValues: slider.stopIndicatorValues + property bool usePercentTooltip: true + property real from: slider.from + property real to: slider.to + property real textWidth: 120 + + RowLayout { + id: row + spacing: 10 + + OptionalMaterialSymbol { + id: iconWidget + icon: root.buttonIcon + iconSize: Appearance.font.pixelSize.larger + } + StyledText { + id: labelWidget + Layout.preferredWidth: root.textWidth + text: root.text + color: Appearance.colors.colOnSecondaryContainer + } + } + + StyledSlider { + id: slider + configuration: StyledSlider.Configuration.XS + usePercentTooltip: root.usePercentTooltip + value: root.value + from: root.from + to: root.to + } +} \ No newline at end of file diff --git a/dots/.config/quickshell/ii/modules/common/widgets/DashedBorder.qml b/dots/.config/quickshell/ii/modules/common/widgets/DashedBorder.qml new file mode 100644 index 000000000..1d992ffab --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/DashedBorder.qml @@ -0,0 +1,28 @@ +import QtQuick +import qs.modules.common +import qs.modules.common.functions + +Canvas { + id: root + property color color: "#ffffff" + property int dashLength: 6 + property int gapLength: 4 + property int borderWidth: 1 + + onDashLengthChanged: requestPaint() + onGapLengthChanged: requestPaint() + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + onPaint: { + var ctx = getContext("2d"); + ctx.clearRect(0, 0, width, height); + ctx.save(); + ctx.strokeStyle = root.color; + ctx.lineWidth = root.borderWidth; + if (root.gapLength > 0) { + ctx.setLineDash([root.dashLength, root.gapLength]); // Set dash pattern + } + ctx.strokeRect(root.borderWidth / 2, root.borderWidth / 2, width - root.borderWidth, height - root.borderWidth); // Draw it + ctx.restore(); + } +} diff --git a/dots/.config/quickshell/ii/modules/common/widgets/DragManager.qml b/dots/.config/quickshell/ii/modules/common/widgets/DragManager.qml index 9a430d93b..4d6225400 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/DragManager.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/DragManager.qml @@ -14,12 +14,16 @@ MouseArea { property bool automaticallyReset: true readonly property real dragDiffX: _dragDiffX readonly property real dragDiffY: _dragDiffY + property real startX: 0 + property real startY: 0 + property real regionTopLeftX: Math.min(startX, startX + _dragDiffX) + property real regionTopLeftY: Math.min(startY, startY + _dragDiffY) + property real regionWidth: Math.abs(_dragDiffX) + property real regionHeight: Math.abs(_dragDiffY) signal dragPressed(diffX: real, diffY: real) signal dragReleased(diffX: real, diffY: real) - property real startX: 0 - property real startY: 0 property bool dragging: false property real _dragDiffX: 0 property real _dragDiffY: 0 diff --git a/dots/.config/quickshell/ii/modules/common/widgets/FullscreenPolkitWindow.qml b/dots/.config/quickshell/ii/modules/common/widgets/FullscreenPolkitWindow.qml new file mode 100644 index 000000000..e4c6ef725 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/FullscreenPolkitWindow.qml @@ -0,0 +1,44 @@ +pragma ComponentBehavior: Bound +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import QtQuick +import Quickshell +import Quickshell.Wayland + +Scope { + id: root + required property Component contentComponent + + Loader { + active: PolkitService.active + sourceComponent: Variants { + model: Quickshell.screens + delegate: PanelWindow { + id: panelWindow + required property var modelData + screen: modelData + + anchors { + top: true + left: true + right: true + bottom: true + } + + color: "transparent" + WlrLayershell.namespace: "quickshell:polkit" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + WlrLayershell.layer: WlrLayer.Overlay + exclusionMode: ExclusionMode.Ignore + + Loader { + anchors.fill: parent + sourceComponent: root.contentComponent + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml index b590f22ac..ad250d9b6 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml @@ -1,5 +1,5 @@ import qs.modules.common -import "notification_utils.js" as NotificationUtils +import qs.modules.common.functions import Qt5Compat.GraphicalEffects import QtQuick import Quickshell diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml index b20b470fa..e3d64027b 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml @@ -1,7 +1,6 @@ import qs.services import qs.modules.common import qs.modules.common.functions -import "notification_utils.js" as NotificationUtils import QtQuick import QtQuick.Layouts import Quickshell @@ -122,7 +121,7 @@ MouseArea { // Notification group area id: background anchors.left: parent.left width: parent.width - color: popup ? ColorUtils.applyAlpha(Appearance.colors.colLayer2, 1 - Appearance.backgroundTransparency) : Appearance.colors.colLayer2 + color: popup ? Appearance.colors.colBackgroundSurfaceContainer : Appearance.colors.colLayer2 radius: Appearance.rounding.normal anchors.leftMargin: root.xOffset @@ -136,7 +135,7 @@ MouseArea { // Notification group area } clip: true - implicitHeight: expanded ? + implicitHeight: root.expanded ? row.implicitHeight + padding * 2 : Math.min(80, row.implicitHeight + padding * 2) @@ -157,8 +156,8 @@ MouseArea { // Notification group area Layout.alignment: Qt.AlignTop Layout.fillWidth: false image: root?.multipleNotifications ? "" : notificationGroup?.notifications[0]?.image ?? "" - appIcon: notificationGroup?.appIcon - summary: notificationGroup?.notifications[root.notificationCount - 1]?.summary + appIcon: root.notificationGroup?.appIcon + summary: root.notificationGroup?.notifications[root.notificationCount - 1]?.summary urgency: root.notifications.some(n => n.urgency === NotificationUrgency.Critical.toString()) ? NotificationUrgency.Critical : NotificationUrgency.Normal } diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml index 7ae160767..bb1bab787 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml @@ -31,28 +31,6 @@ Item { // Notification item area implicitHeight: background.implicitHeight - function processNotificationBody(body, appName) { - let processedBody = body - - // Clean Chromium-based browsers notifications - remove first line - if (appName) { - const lowerApp = appName.toLowerCase() - const chromiumBrowsers = [ - "brave", "chrome", "chromium", "vivaldi", "opera", "microsoft edge" - ] - - if (chromiumBrowsers.some(name => lowerApp.includes(name))) { - const lines = body.split('\n\n') - - if (lines.length > 1 && lines[0].startsWith('") + return NotificationUtils.processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "
") } } } ColumnLayout { // Expanded content + id: expandedContentColumn Layout.fillWidth: true opacity: root.expanded ? 1 : 0 visible: opacity > 0 @@ -218,8 +197,8 @@ Item { // Notification item area elide: Text.ElideRight textFormat: Text.RichText text: { - return `` + - `${processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "
")}` + return `` + + `${NotificationUtils.processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "
")}` } onLinkActivated: (link) => { @@ -293,6 +272,8 @@ Item { // Notification item area id: actionRepeater model: notificationObject.actions NotificationActionButton { + id: notifAction + required property var modelData Layout.fillWidth: true buttonText: modelData.text urgency: notificationObject.urgency diff --git a/dots/.config/quickshell/ii/modules/common/widgets/SelectionDialog.qml b/dots/.config/quickshell/ii/modules/common/widgets/SelectionDialog.qml index 68d67a32c..6100c0864 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/SelectionDialog.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/SelectionDialog.qml @@ -32,7 +32,7 @@ Item { Rectangle { // The dialog id: dialog - color: Appearance.colors.colSurfaceContainerHigh + color: Appearance.m3colors.m3surfaceContainerHigh radius: Appearance.rounding.normal anchors.fill: parent anchors.margins: dialogMargin diff --git a/dots/.config/quickshell/ii/modules/common/widgets/StyledComboBox.qml b/dots/.config/quickshell/ii/modules/common/widgets/StyledComboBox.qml index 18add2cc3..55504acb2 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/StyledComboBox.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/StyledComboBox.qml @@ -192,7 +192,7 @@ ComboBox { id: popupBackground anchors.fill: parent radius: Appearance.rounding.normal - color: Appearance.colors.colSurfaceContainerHigh + color: Appearance.m3colors.m3surfaceContainerHigh } } diff --git a/dots/.config/quickshell/ii/modules/common/widgets/StyledImage.qml b/dots/.config/quickshell/ii/modules/common/widgets/StyledImage.qml index c360b536c..17dfc56c4 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/StyledImage.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/StyledImage.qml @@ -12,4 +12,14 @@ Image { Behavior on opacity { animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) } + + property list fallbacks: [] + property int currentFallbackIndex: 0 + + onStatusChanged: { + if (status === Image.Error && currentFallbackIndex < fallbacks.length) { + source = fallbacks[currentFallbackIndex]; + currentFallbackIndex += 1; + } + } } diff --git a/dots/.config/quickshell/ii/modules/common/widgets/StyledListView.qml b/dots/.config/quickshell/ii/modules/common/widgets/StyledListView.qml index 2db1f094a..4267e7bcd 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/StyledListView.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/StyledListView.qml @@ -54,6 +54,7 @@ ListView { Behavior on contentY { NumberAnimation { id: scrollAnim + alwaysRunToEnd: true duration: Appearance.animation.scroll.duration easing.type: Appearance.animation.scroll.type easing.bezierCurve: Appearance.animation.scroll.bezierCurve @@ -99,7 +100,7 @@ ListView { to: 1, }), ] : [] - } + } move: Transition { animations: root.animateMovement ? [ diff --git a/dots/.config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml b/dots/.config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml index 7b677c426..f37203250 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml @@ -9,6 +9,7 @@ ScrollBar { policy: ScrollBar.AsNeeded topPadding: Appearance.rounding.normal bottomPadding: Appearance.rounding.normal + active: hovered || pressed contentItem: Rectangle { implicitWidth: 4 diff --git a/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml b/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml index 98b8eebf3..971a1eb10 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml @@ -46,7 +46,8 @@ Slider { property real handleWidth: root.pressed ? handlePressedWidth : handleDefaultWidth property real handleMargins: 4 property real trackDotSize: 3 - property string tooltipContent: `${Math.round(value * 100)}%` + property bool usePercentTooltip: true + property string tooltipContent: usePercentTooltip ? `${Math.round(((value - from) / (to - from)) * 100)}%` : `${Math.round(value)}` property bool wavy: configuration === StyledSlider.Configuration.Wavy // If true, the progress bar will have a wavy fill effect property bool animateWave: true property real waveAmplitudeMultiplier: wavy ? 0.5 : 0 diff --git a/dots/.config/quickshell/ii/modules/common/widgets/StyledToolTip.qml b/dots/.config/quickshell/ii/modules/common/widgets/StyledToolTip.qml index 53797fb66..4688b29be 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/StyledToolTip.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/StyledToolTip.qml @@ -20,6 +20,7 @@ ToolTip { hintingPreference: Font.PreferNoHinting // Prevent shaky text } + delay: 0 visible: internalVisibleCondition contentItem: StyledToolTipContent { diff --git a/dots/.config/quickshell/ii/modules/common/widgets/ToolbarTabBar.qml b/dots/.config/quickshell/ii/modules/common/widgets/ToolbarTabBar.qml index 1bca18463..708b30bd7 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/ToolbarTabBar.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/ToolbarTabBar.qml @@ -12,19 +12,30 @@ Item { required property var tabButtonList function incrementCurrentIndex() { - tabBar.incrementCurrentIndex() + tabBar.incrementCurrentIndex(); } function decrementCurrentIndex() { - tabBar.decrementCurrentIndex() + tabBar.decrementCurrentIndex(); } function setCurrentIndex(index) { - tabBar.setCurrentIndex(index) + tabBar.setCurrentIndex(index); } Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter implicitWidth: contentItem.implicitWidth implicitHeight: 40 + property Component delegate: ToolbarTabButton { + required property int index + required property var modelData + current: index == root.currentIndex + text: modelData.name + materialSymbol: modelData.icon + onClicked: { + root.setCurrentIndex(index); + } + } + Row { id: contentItem z: 1 @@ -33,16 +44,7 @@ Item { Repeater { model: root.tabButtonList - delegate: ToolbarTabButton { - required property int index - required property var modelData - current: index == root.currentIndex - text: modelData.name - materialSymbol: modelData.icon - onClicked: { - root.setCurrentIndex(index) - } - } + delegate: root.delegate } } @@ -76,23 +78,23 @@ Item { z: 2 acceptedButtons: Qt.NoButton cursorShape: Qt.PointingHandCursor - onWheel: (event) => { + onWheel: event => { if (event.angleDelta.y < 0) { root.incrementCurrentIndex(); - } - else { + } else { root.decrementCurrentIndex(); } } } - // TabBar doesn't allow tabs to be of different sizes. Literally unusable. + // TabBar doesn't allow tabs to be of different sizes. That's what I thought... // We use it only for the logic and draw stuff manually TabBar { id: tabBar z: -1 background: null - Repeater { // This is to fool the TabBar that it has tabs so it does the indices properly + Repeater { + // This is to fool the TabBar that it has tabs so it does the indices properly model: root.tabButtonList.length delegate: TabButton { background: null diff --git a/dots/.config/quickshell/ii/modules/common/widgets/WeekRow.qml b/dots/.config/quickshell/ii/modules/common/widgets/WeekRow.qml new file mode 100644 index 000000000..93fc536d2 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/WeekRow.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.services +import qs.modules.common.functions + +RowLayout { + id: root + + // Pls supply + required property date date // Any date within the week + property var locale + + // Expose model and delegate for flexibility + property list model: { + // Should expose props like here: https://doc.qt.io/qt-6/qml-qtquick-controls-monthgrid.html#delegate-prop + // (except weekNumber because i'm lazy and it's not so important) + const firstDayOfWeek = DateUtils.getFirstDayOfWeek(root.date, root.locale.firstDayOfWeek); + const weekDates = []; + for (let i = 0; i < 7; i++) { + const dayDate = new Date(firstDayOfWeek); + dayDate.setDate(firstDayOfWeek.getDate() + i); + weekDates.push({ + date: dayDate, + day: dayDate.getDate(), + month: dayDate.getMonth() + 1, + year: dayDate.getFullYear(), + today: DateUtils.sameDate(dayDate, DateTime.clock.date) + }); + } + return weekDates; + } + property Component delegate: Text { + required property var model + text: model.day + } + + // Obvious + Repeater { + model: root.model + delegate: root.delegate + } +} diff --git a/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js b/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js deleted file mode 100644 index 1f879baa2..000000000 --- a/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js +++ /dev/null @@ -1,82 +0,0 @@ - -/** - * @param { string } summary - * @returns { string } - */ -function findSuitableMaterialSymbol(summary = "") { - const defaultType = 'chat'; - if(summary.length === 0) return defaultType; - - const keywordsToTypes = { - 'reboot': 'restart_alt', - 'record': 'screen_record', - 'battery': 'power', - 'power': 'power', - 'screenshot': 'screenshot_monitor', - 'welcome': 'waving_hand', - 'time': 'scheduleb', - 'installed': 'download', - 'configuration reloaded': 'reset_wrench', - 'unable': 'question_mark', - "couldn't": 'question_mark', - 'config': 'reset_wrench', - 'update': 'update', - 'ai response': 'neurology', - 'control': 'settings', - 'upsca': 'compare', - 'music': 'queue_music', - 'install': 'deployed_code_update', - 'input': 'keyboard_alt', - 'preedit': 'keyboard_alt', - 'startswith:file': 'folder_copy', // Declarative startsWith check - }; - - const lowerSummary = summary.toLowerCase(); - - for (const [keyword, type] of Object.entries(keywordsToTypes)) { - if (keyword.startsWith('startswith:')) { - const startsWithKeyword = keyword.replace('startswith:', ''); - if (lowerSummary.startsWith(startsWithKeyword)) { - return type; - } - } else if (lowerSummary.includes(keyword)) { - return type; - } - } - - return defaultType; -} - -/** - * @param { number | string | Date } timestamp - * @returns { string } - */ -const getFriendlyNotifTimeString = (timestamp) => { - if (!timestamp) return ''; - const messageTime = new Date(timestamp); - const now = new Date(); - const diffMs = now.getTime() - messageTime.getTime(); - - // Less than 1 minute - if (diffMs < 60000) - return 'Now'; - - // Same day - show relative time - if (messageTime.toDateString() === now.toDateString()) { - const diffMinutes = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - - if (diffHours > 0) { - return `${diffHours}h`; - } else { - return `${diffMinutes}m`; - } - } - - // Yesterday - if (messageTime.toDateString() === new Date(now.getTime() - 86400000).toDateString()) - return 'Yesterday'; - - // Older dates - return Qt.formatDateTime(messageTime, "MMMM dd"); -}; \ No newline at end of file diff --git a/dots/.config/quickshell/ii/modules/ii/background/widgets/AbstractBackgroundWidget.qml b/dots/.config/quickshell/ii/modules/ii/background/widgets/AbstractBackgroundWidget.qml index b24c33aaa..6d8c14598 100644 --- a/dots/.config/quickshell/ii/modules/ii/background/widgets/AbstractBackgroundWidget.qml +++ b/dots/.config/quickshell/ii/modules/ii/background/widgets/AbstractBackgroundWidget.qml @@ -59,7 +59,8 @@ AbstractWidget { function onReadyChanged() { refreshPlacementIfNeeded() } } function refreshPlacementIfNeeded() { - if (!Config.ready || (root.placementStrategy === "free" && root.needsColText)) return; + if (!Config.ready) return; + if (root.placementStrategy === "free" && !root.needsColText) return; leastBusyRegionProc.wallpaperPath = root.wallpaperPath; leastBusyRegionProc.running = false; leastBusyRegionProc.running = true; diff --git a/dots/.config/quickshell/ii/modules/ii/background/widgets/clock/ClockText.qml b/dots/.config/quickshell/ii/modules/ii/background/widgets/clock/ClockText.qml new file mode 100644 index 000000000..133a2759c --- /dev/null +++ b/dots/.config/quickshell/ii/modules/ii/background/widgets/clock/ClockText.qml @@ -0,0 +1,19 @@ +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Layouts + +StyledText { + Layout.fillWidth: true + font { + family: Appearance.font.family.expressive + pixelSize: 20 + weight: 350 + // Set empty to prevent conflicts, not meaningless + styleName: "" + variableAxes: ({}) + } + style: Text.Raised + styleColor: Appearance.colors.colShadow + animateChange: Config.options.background.widgets.clock.digital.animateChange +} \ No newline at end of file diff --git a/dots/.config/quickshell/ii/modules/ii/background/widgets/clock/ClockWidget.qml b/dots/.config/quickshell/ii/modules/ii/background/widgets/clock/ClockWidget.qml index 97e1e468a..964755269 100644 --- a/dots/.config/quickshell/ii/modules/ii/background/widgets/clock/ClockWidget.qml +++ b/dots/.config/quickshell/ii/modules/ii/background/widgets/clock/ClockWidget.qml @@ -26,7 +26,7 @@ AbstractBackgroundWidget { visibleWhenLocked: true property var textHorizontalAlignment: { - if (root.forceCenter) + if (!Config.options.background.widgets.clock.digital.adaptiveAlignment || root.forceCenter) return Text.AlignHCenter; if (root.x < root.scaledScreenWidth / 3) return Text.AlignLeft; @@ -63,32 +63,9 @@ AbstractBackgroundWidget { anchors.horizontalCenter: parent.horizontalCenter shown: root.clockStyle === "digital" && (root.shouldShow) fade: false - sourceComponent: ColumnLayout { - id: clockColumn - spacing: 6 - - ClockText { - font.pixelSize: 90 - text: DateTime.time - } - ClockText { - Layout.topMargin: -5 - text: DateTime.longDate - } - StyledText { - // Somehow gets fucked up if made a ClockText??? - visible: Config.options.background.widgets.clock.quote.enable && Config.options.background.widgets.clock.quote.text.length > 0 - Layout.fillWidth: true - horizontalAlignment: root.textHorizontalAlignment - font { - pixelSize: Appearance.font.pixelSize.normal - weight: 350 - } - color: root.colText - style: Text.Raised - styleColor: Appearance.colors.colShadow - text: Config.options.background.widgets.clock.quote.text - } + sourceComponent: DigitalClock { + colText: root.colText + textHorizontalAlignment: root.textHorizontalAlignment } } StatusRow { @@ -154,19 +131,6 @@ AbstractBackgroundWidget { } } - component ClockText: StyledText { - Layout.fillWidth: true - horizontalAlignment: root.textHorizontalAlignment - font { - family: Appearance.font.family.expressive - pixelSize: 20 - weight: Font.DemiBold - } - color: root.colText - style: Text.Raised - styleColor: Appearance.colors.colShadow - animateChange: Config.options.background.widgets.clock.digital.animateChange - } component ClockStatusText: Row { id: statusTextRow property alias statusIcon: statusIconWidget.text @@ -190,6 +154,7 @@ AbstractBackgroundWidget { ClockText { id: statusTextWidget color: statusTextRow.textColor + horizontalAlignment: root.textHorizontalAlignment anchors.verticalCenter: statusTextRow.verticalCenter font { pixelSize: Appearance.font.pixelSize.large diff --git a/dots/.config/quickshell/ii/modules/ii/background/widgets/clock/DigitalClock.qml b/dots/.config/quickshell/ii/modules/ii/background/widgets/clock/DigitalClock.qml new file mode 100644 index 000000000..d2a6f95d0 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/ii/background/widgets/clock/DigitalClock.qml @@ -0,0 +1,69 @@ +pragma ComponentBehavior: Bound + +import qs.services +import qs.modules.common +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: clockColumn + spacing: 4 + + property bool isVertical: Config.options.background.widgets.clock.digital.vertical + property color colText: Appearance.colors.colOnSecondaryContainer + property var textHorizontalAlignment: Text.AlignHCenter + + // Time + ClockText { + id: timeTextTop + text: clockColumn.isVertical ? DateTime.time.split(":")[0].padStart(2, "0") : DateTime.time + color: clockColumn.colText + horizontalAlignment: Text.AlignHCenter + font { + pixelSize: Config.options.background.widgets.clock.digital.font.size + weight: Config.options.background.widgets.clock.digital.font.weight + family: Config.options.background.widgets.clock.digital.font.family + variableAxes: ({ + "wdth": Config.options.background.widgets.clock.digital.font.width, + "ROND": Config.options.background.widgets.clock.digital.font.roundness + }) + } + } + + Loader { + Layout.topMargin: -40 + active: clockColumn.isVertical + visible: active + sourceComponent: ClockText { + id: timeTextBottom + text: DateTime.time.split(":")[1].split(" ")[0].padStart(2, "0") + color: clockColumn.colText + horizontalAlignment: Text.AlignHCenter + font { + pixelSize: timeTextTop.font.pixelSize + weight: timeTextTop.font.weight + family: timeTextTop.font.family + variableAxes: timeTextTop.font.variableAxes + } + } + } + + // Date + ClockText { + visible: Config.options.background.widgets.clock.digital.showDate + Layout.topMargin: -20 + text: DateTime.longDate + color: clockColumn.colText + horizontalAlignment: clockColumn.textHorizontalAlignment + } + + // Quote + ClockText { + visible: Config.options.background.widgets.clock.quote.enable && Config.options.background.widgets.clock.quote.text.length > 0 + font.pixelSize: Appearance.font.pixelSize.normal + text: Config.options.background.widgets.clock.quote.text + animateChange: false + color: clockColumn.colText + horizontalAlignment: clockColumn.textHorizontalAlignment + } +} diff --git a/dots/.config/quickshell/ii/modules/ii/bar/BatteryIndicator.qml b/dots/.config/quickshell/ii/modules/ii/bar/BatteryIndicator.qml index 36799fefd..176f3a065 100644 --- a/dots/.config/quickshell/ii/modules/ii/bar/BatteryIndicator.qml +++ b/dots/.config/quickshell/ii/modules/ii/bar/BatteryIndicator.qml @@ -30,7 +30,11 @@ MouseArea { height: batteryProgress.valueBarHeight RowLayout { - anchors.centerIn: parent + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: (parent.height - height) / 2 + } spacing: 0 MaterialSymbol { diff --git a/dots/.config/quickshell/ii/modules/ii/cheatsheet/Cheatsheet.qml b/dots/.config/quickshell/ii/modules/ii/cheatsheet/Cheatsheet.qml index d133fa654..f7fb68f68 100644 --- a/dots/.config/quickshell/ii/modules/ii/cheatsheet/Cheatsheet.qml +++ b/dots/.config/quickshell/ii/modules/ii/cheatsheet/Cheatsheet.qml @@ -4,6 +4,7 @@ import qs.modules.common.widgets import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Qt.labs.synchronizer import Qt5Compat.GraphicalEffects import Quickshell.Io import Quickshell @@ -135,7 +136,10 @@ Scope { // Scope ToolbarTabBar { id: tabBar tabButtonList: root.tabButtonList - currentIndex: swipeView.currentIndex + + Synchronizer on currentIndex { + property alias source: swipeView.currentIndex + } } } @@ -144,8 +148,11 @@ Scope { // Scope Layout.topMargin: 5 Layout.fillWidth: true Layout.fillHeight: true - currentIndex: tabBar.currentIndex spacing: 10 + currentIndex: Persistent.states.cheatsheet.tabIndex + onCurrentIndexChanged: { + Persistent.states.cheatsheet.tabIndex = currentIndex; + } implicitWidth: Math.max.apply(null, contentChildren.map(child => child.implicitWidth || 0)) implicitHeight: Math.max.apply(null, contentChildren.map(child => child.implicitHeight || 0)) diff --git a/dots/.config/quickshell/ii/modules/ii/lock/Lock.qml b/dots/.config/quickshell/ii/modules/ii/lock/Lock.qml index e942f4fda..3aae6ef7b 100644 --- a/dots/.config/quickshell/ii/modules/ii/lock/Lock.qml +++ b/dots/.config/quickshell/ii/modules/ii/lock/Lock.qml @@ -3,110 +3,39 @@ import qs import qs.services import qs.modules.common import qs.modules.common.functions +import qs.modules.common.panels.lock import QtQuick import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Hyprland -Scope { +LockScreen { id: root - Process { - id: unlockKeyringProc - onExited: (exitCode, exitStatus) => { - KeyringStorage.fetchKeyringData(); - } - } - function unlockKeyring() { - unlockKeyringProc.exec({ - environment: ({ - "UNLOCK_PASSWORD": lockContext.currentText - }), - command: ["bash", "-c", Quickshell.shellPath("scripts/keyring/unlock.sh")] - }) + lockSurface: LockSurface { + context: root.context } + // Push everything down property var windowData: [] function saveWindowPositionAndTile() { - Quickshell.execDetached(["hyprctl", "keyword", "dwindle:pseudotile", "true"]) - root.windowData = HyprlandData.windowList.filter(w => (w.floating && w.workspace.id === HyprlandData.activeWorkspace.id)) + Quickshell.execDetached(["hyprctl", "keyword", "dwindle:pseudotile", "true"]); + root.windowData = HyprlandData.windowList.filter(w => (w.floating && w.workspace.id === HyprlandData.activeWorkspace.id)); root.windowData.forEach(w => { - Hyprland.dispatch(`pseudo address:${w.address}`) - Hyprland.dispatch(`settiled address:${w.address}`) - Hyprland.dispatch(`movetoworkspacesilent ${w.workspace.id},address:${w.address}`) - }) + Hyprland.dispatch(`pseudo address:${w.address}`); + Hyprland.dispatch(`settiled address:${w.address}`); + Hyprland.dispatch(`movetoworkspacesilent ${w.workspace.id},address:${w.address}`); + }); } function restoreWindowPositionAndTile() { root.windowData.forEach(w => { - Hyprland.dispatch(`setfloating address:${w.address}`) - Hyprland.dispatch(`movewindowpixel exact ${w.at[0]} ${w.at[1]}, address:${w.address}`) - Hyprland.dispatch(`pseudo address:${w.address}`) - }) - Quickshell.execDetached(["hyprctl", "keyword", "dwindle:pseudotile", "false"]) + Hyprland.dispatch(`setfloating address:${w.address}`); + Hyprland.dispatch(`movewindowpixel exact ${w.at[0]} ${w.at[1]}, address:${w.address}`); + Hyprland.dispatch(`pseudo address:${w.address}`); + }); + Quickshell.execDetached(["hyprctl", "keyword", "dwindle:pseudotile", "false"]); } - - // This stores all the information shared between the lock surfaces on each screen. - // https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen - LockContext { - id: lockContext - - Connections { - target: GlobalStates - function onScreenLockedChanged() { - if (GlobalStates.screenLocked) { - lockContext.reset(); - lockContext.tryFingerUnlock(); - } - } - } - - onUnlocked: (targetAction) => { - // Perform the target action if it's not just unlocking - if (targetAction == LockContext.ActionEnum.Poweroff) { - Session.poweroff(); - return; - } else if (targetAction == LockContext.ActionEnum.Reboot) { - Session.reboot(); - return; - } - - // Unlock the keyring if configured to do so - if (Config.options.lock.security.unlockKeyring) root.unlockKeyring(); - - // Unlock the screen before exiting, or the compositor will display a - // fallback lock you can't interact with. - GlobalStates.screenLocked = false; - - // Refocus last focused window on unlock (hack) - Quickshell.execDetached(["bash", "-c", `sleep 0.2; hyprctl --batch "dispatch togglespecialworkspace; dispatch togglespecialworkspace"`]) - - // Reset - lockContext.reset(); - } - } - - WlSessionLock { - id: lock - locked: GlobalStates.screenLocked - - WlSessionLockSurface { - color: "transparent" - Loader { - active: GlobalStates.screenLocked - anchors.fill: parent - opacity: active ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) - } - sourceComponent: LockSurface { - context: lockContext - } - } - } - } - - // Blur layer hack Variants { model: Quickshell.screens delegate: Scope { @@ -118,71 +47,12 @@ Scope { onShouldPushChanged: { if (shouldPush) { root.saveWindowPositionAndTile(); - Quickshell.execDetached(["bash", "-c", `hyprctl keyword monitor ${targetMonitorName}, addreserved, ${verticalMovementDistance}, ${-verticalMovementDistance}, ${horizontalSqueeze}, ${horizontalSqueeze}`]) + Quickshell.execDetached(["bash", "-c", `hyprctl keyword monitor ${targetMonitorName}, addreserved, ${verticalMovementDistance}, ${-verticalMovementDistance}, ${horizontalSqueeze}, ${horizontalSqueeze}`]); } else { - Quickshell.execDetached(["bash", "-c", `hyprctl keyword monitor ${targetMonitorName}, addreserved, 0, 0, 0, 0`]) + Quickshell.execDetached(["bash", "-c", `hyprctl keyword monitor ${targetMonitorName}, addreserved, 0, 0, 0, 0`]); root.restoreWindowPositionAndTile(); } } } } - - function lock() { - if (Config.options.lock.useHyprlock) { - Quickshell.execDetached(["bash", "-c", "pidof hyprlock || hyprlock"]); - return; - } - GlobalStates.screenLocked = true; - } - - IpcHandler { - target: "lock" - - function activate(): void { - root.lock(); - } - function focus(): void { - lockContext.shouldReFocus(); - } - } - - GlobalShortcut { - name: "lock" - description: "Locks the screen" - - onPressed: { - root.lock() - } - } - - GlobalShortcut { - name: "lockFocus" - description: "Re-focuses the lock screen. This is because Hyprland after waking up for whatever reason" - + "decides to keyboard-unfocus the lock screen" - - onPressed: { - lockContext.shouldReFocus(); - } - } - - function initIfReady() { - if (!Config.ready || !Persistent.ready) return; - if (Config.options.lock.launchOnStartup && Persistent.isNewHyprlandInstance) { - root.lock(); - } else { - KeyringStorage.fetchKeyringData(); - } - } - Connections { - target: Config - function onReadyChanged() { - root.initIfReady(); - } - } - Connections { - target: Persistent - function onReadyChanged() { - root.initIfReady(); - } - } } diff --git a/dots/.config/quickshell/ii/modules/ii/lock/LockSurface.qml b/dots/.config/quickshell/ii/modules/ii/lock/LockSurface.qml index da3e17627..b2482132d 100644 --- a/dots/.config/quickshell/ii/modules/ii/lock/LockSurface.qml +++ b/dots/.config/quickshell/ii/modules/ii/lock/LockSurface.qml @@ -7,6 +7,7 @@ import qs.services import qs.modules.common import qs.modules.common.widgets import qs.modules.common.functions +import qs.modules.common.panels.lock import qs.modules.ii.bar as Bar import Quickshell import Quickshell.Services.SystemTray @@ -59,10 +60,20 @@ MouseArea { } // Key presses + property bool ctrlHeld: false Keys.onPressed: event => { root.context.resetClearTimer(); + if (event.key === Qt.Key_Control) { + root.ctrlHeld = true; + } if (event.key === Qt.Key_Escape) { // Esc to clear root.context.currentText = ""; + } + forceFieldFocus(); + } + Keys.onReleased: event => { + if (event.key === Qt.Key_Control) { + root.ctrlHeld = false; } forceFieldFocus(); } @@ -133,7 +144,9 @@ MouseArea { // Synchronizing (across monitors) and unlocking onTextChanged: root.context.currentText = this.text - onAccepted: root.context.tryUnlock() + onAccepted: { + root.context.tryUnlock(ctrlHeld); + } Connections { target: root.context function onCurrentTextChanged() { @@ -202,7 +215,7 @@ MouseArea { iconSize: 24 text: { if (root.context.targetAction === LockContext.ActionEnum.Unlock) { - return "arrow_right_alt"; + return root.ctrlHeld ? "emoji_food_beverage" : "arrow_right_alt"; } else if (root.context.targetAction === LockContext.ActionEnum.Poweroff) { return "power_settings_new"; } else if (root.context.targetAction === LockContext.ActionEnum.Reboot) { diff --git a/dots/.config/quickshell/ii/modules/ii/mediaControls/MediaControls.qml b/dots/.config/quickshell/ii/modules/ii/mediaControls/MediaControls.qml index fa60e0474..3cd620834 100644 --- a/dots/.config/quickshell/ii/modules/ii/mediaControls/MediaControls.qml +++ b/dots/.config/quickshell/ii/modules/ii/mediaControls/MediaControls.qml @@ -16,7 +16,7 @@ Scope { id: root property bool visible: false readonly property MprisPlayer activePlayer: MprisController.activePlayer - readonly property var realPlayers: Mpris.players.values.filter(player => isRealPlayer(player)) + readonly property var realPlayers: MprisController.players readonly property var meaningfulPlayers: filterDuplicatePlayers(realPlayers) readonly property real osdWidth: Appearance.sizes.osdWidth readonly property real widgetWidth: Appearance.sizes.mediaControlsWidth @@ -24,27 +24,6 @@ Scope { property real popupRounding: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 property list visualizerPoints: [] - property bool hasPlasmaIntegration: false - Process { - id: plasmaIntegrationAvailabilityCheckProc - running: true - command: ["bash", "-c", "command -v plasma-browser-integration-host"] - onExited: (exitCode, exitStatus) => { - root.hasPlasmaIntegration = (exitCode === 0); - } - } - function isRealPlayer(player) { - if (!Config.options.media.filterDuplicatePlayers) { - return true; - } - return ( - // Remove unecessary native buses from browsers if there's plasma integration - !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) && !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) && - // playerctld just copies other buses and we don't need duplicates - !player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') && - // Non-instance mpd bus - !(player.dbusName?.endsWith('.mpd') && !player.dbusName.endsWith('MediaPlayer2.mpd'))); - } function filterDuplicatePlayers(players) { let filtered = []; let used = new Set(); @@ -96,7 +75,7 @@ Scope { id: mediaControlsLoader active: GlobalStates.mediaControlsOpen onActiveChanged: { - if (!mediaControlsLoader.active && Mpris.players.values.filter(player => isRealPlayer(player)).length === 0) { + if (!mediaControlsLoader.active && root.realPlayers.length === 0) { GlobalStates.mediaControlsOpen = false; } } diff --git a/dots/.config/quickshell/ii/modules/ii/overlay/OverlayBackground.qml b/dots/.config/quickshell/ii/modules/ii/overlay/OverlayBackground.qml index d91e4cbba..205307cab 100644 --- a/dots/.config/quickshell/ii/modules/ii/overlay/OverlayBackground.qml +++ b/dots/.config/quickshell/ii/modules/ii/overlay/OverlayBackground.qml @@ -4,5 +4,5 @@ import qs.modules.common Rectangle { id: contentItem anchors.fill: parent - color: Appearance.colors.colSurfaceContainer + color: Appearance.m3colors.m3surfaceContainer } diff --git a/dots/.config/quickshell/ii/modules/ii/overlay/StyledOverlayWidget.qml b/dots/.config/quickshell/ii/modules/ii/overlay/StyledOverlayWidget.qml index 949cd2380..b3cf1e74a 100644 --- a/dots/.config/quickshell/ii/modules/ii/overlay/StyledOverlayWidget.qml +++ b/dots/.config/quickshell/ii/modules/ii/overlay/StyledOverlayWidget.qml @@ -190,7 +190,7 @@ AbstractOverlayWidget { fill: parent margins: root.resizeMargin } - color: ColorUtils.transparentize(Appearance.colors.colLayer1, (root.fancyBorders && GlobalStates.overlayOpen) ? 0 : 1) + color: ColorUtils.transparentize(Appearance.colors.colLayer1Base, (root.fancyBorders && GlobalStates.overlayOpen) ? 0 : 1) radius: root.radius border.color: ColorUtils.transparentize(Appearance.colors.colOutlineVariant, GlobalStates.overlayOpen ? 0 : 1) border.width: 1 @@ -217,7 +217,7 @@ AbstractOverlayWidget { Layout.fillWidth: true implicitWidth: titleBarRow.implicitWidth + root.padding * 2 implicitHeight: titleBarRow.implicitHeight + root.padding * 2 - color: root.fancyBorders ? "transparent" : Appearance.colors.colLayer1 + color: root.fancyBorders ? "transparent" : Appearance.colors.colLayer1Base // border.color: Appearance.colors.colOutlineVariant // border.width: 1 diff --git a/dots/.config/quickshell/ii/modules/ii/overlay/recorder/Recorder.qml b/dots/.config/quickshell/ii/modules/ii/overlay/recorder/Recorder.qml index 9efa37e60..812884127 100644 --- a/dots/.config/quickshell/ii/modules/ii/overlay/recorder/Recorder.qml +++ b/dots/.config/quickshell/ii/modules/ii/overlay/recorder/Recorder.qml @@ -3,6 +3,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import qs +import qs.services import qs.modules.common import qs.modules.common.widgets import qs.modules.ii.overlay diff --git a/dots/.config/quickshell/ii/modules/ii/overview/Overview.qml b/dots/.config/quickshell/ii/modules/ii/overview/Overview.qml index 248b46b56..bf8706eb0 100644 --- a/dots/.config/quickshell/ii/modules/ii/overview/Overview.qml +++ b/dots/.config/quickshell/ii/modules/ii/overview/Overview.qml @@ -162,7 +162,7 @@ Scope { } IpcHandler { - target: "overview" + target: "search" function toggle() { GlobalStates.overviewOpen = !GlobalStates.overviewOpen; @@ -185,13 +185,21 @@ Scope { } GlobalShortcut { - name: "overviewToggle" - description: "Toggles overview on press" + name: "searchToggle" + description: "Toggles search on press" onPressed: { GlobalStates.overviewOpen = !GlobalStates.overviewOpen; } } + GlobalShortcut { + name: "overviewWorkspacesClose" + description: "Closes overview on press" + + onPressed: { + GlobalStates.overviewOpen = false; + } + } GlobalShortcut { name: "overviewWorkspacesToggle" description: "Toggles overview on press" @@ -201,16 +209,8 @@ Scope { } } GlobalShortcut { - name: "overviewClose" - description: "Closes overview" - - onPressed: { - GlobalStates.overviewOpen = false; - } - } - GlobalShortcut { - name: "overviewToggleRelease" - description: "Toggles overview on release" + name: "searchToggleRelease" + description: "Toggles search on release" onPressed: { GlobalStates.superReleaseMightTrigger = true; @@ -225,8 +225,8 @@ Scope { } } GlobalShortcut { - name: "overviewToggleReleaseInterrupt" - description: "Interrupts possibility of overview being toggled on release. " + "This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. " + "To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything." + name: "searchToggleReleaseInterrupt" + description: "Interrupts possibility of search being toggled on release. " + "This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. " + "To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything." onPressed: { GlobalStates.superReleaseMightTrigger = false; diff --git a/dots/.config/quickshell/ii/modules/ii/overview/OverviewWidget.qml b/dots/.config/quickshell/ii/modules/ii/overview/OverviewWidget.qml index 18501b082..f1ea1b331 100644 --- a/dots/.config/quickshell/ii/modules/ii/overview/OverviewWidget.qml +++ b/dots/.config/quickshell/ii/modules/ii/overview/OverviewWidget.qml @@ -50,6 +50,21 @@ Item { property Component windowComponent: OverviewWindow {} property list windowWidgets: [] + + function getWsRow(ws) { + // 1-indexed workspace, 0-indexed row + var normalRow = Math.floor((ws - 1) / Config.options.overview.columns) % Config.options.overview.rows; + return (Config.options.overview.orderBottomUp ? Config.options.overview.rows - normalRow - 1 : normalRow); + } + function getWsColumn(ws) { + // 1-indexed workspace, 0-indexed column + var normalCol = (ws - 1) % Config.options.overview.columns; + return (Config.options.overview.orderRightLeft ? Config.options.overview.columns - normalCol - 1 : normalCol); + } + function getWsInCell(ri, ci) { + // 1-indexed workspace, 0-indexed row and column index + return (Config.options.overview.orderBottomUp ? Config.options.overview.rows - ri - 1 : ri) * Config.options.overview.columns + (Config.options.overview.orderRightLeft ? Config.options.overview.columns - ci - 1 : ci) + 1 + } StyledRectangularShadow { target: overviewBackground @@ -85,8 +100,8 @@ Item { id: workspace required property int index property int colIndex: index - property int workspaceValue: root.workspaceGroup * root.workspacesShown + row.index * Config.options.overview.columns + colIndex + 1 - property color defaultWorkspaceColor: ColorUtils.mix(Appearance.colors.colBackgroundSurfaceContainer, Appearance.colors.colSurfaceContainerHigh, 0.8) + property int workspaceValue: root.workspaceGroup * root.workspacesShown + getWsInCell(row.index, colIndex) + property color defaultWorkspaceColor: Appearance.colors.colSurfaceContainerLow property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1) property color hoveredBorderColor: Appearance.colors.colLayer2Hover property bool hoveredWhileDragging: false @@ -159,12 +174,12 @@ Item { model: ScriptModel { values: { // console.log(JSON.stringify(ToplevelManager.toplevels.values.map(t => t), null, 2)) - return [...ToplevelManager.toplevels.values.filter((toplevel) => { + return ToplevelManager.toplevels.values.filter((toplevel) => { const address = `0x${toplevel.HyprlandToplevel?.address}` var win = windowByAddress[address] const inWorkspaceGroup = (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown) return inWorkspaceGroup; - })].reverse() + }) } } delegate: OverviewWindow { @@ -182,8 +197,8 @@ Item { property bool atInitPosition: (initX == x && initY == y) // Offset on the canvas - property int workspaceColIndex: (windowData?.workspace.id - 1) % Config.options.overview.columns - property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / Config.options.overview.columns) + property int workspaceColIndex: getWsColumn(windowData?.workspace.id) + property int workspaceRowIndex: getWsRow(windowData?.workspace.id) xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex property real xWithinWorkspaceWidget: Math.max((windowData?.at[0] - (monitor?.x ?? 0) - monitorData?.reserved[0]) * root.scale, 0) @@ -278,7 +293,7 @@ Item { StyledToolTip { extraVisibleCondition: false alternativeVisibleCondition: dragArea.containsMouse && !window.Drag.active - text: `${windowData.title}\n[${windowData.class}] ${windowData.xwayland ? "[XWayland] " : ""}` + text: `${windowData?.title}\n[${windowData?.class}] ${windowData?.xwayland ? "[XWayland] " : ""}` } } } @@ -286,9 +301,8 @@ Item { Rectangle { // Focused workspace indicator id: focusedWorkspaceIndicator - property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown) - property int rowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.options.overview.columns) - property int colIndex: (activeWorkspaceInGroup - 1) % Config.options.overview.columns + property int rowIndex: getWsRow(monitor.activeWorkspace?.id) + property int colIndex: getWsColumn(monitor.activeWorkspace?.id) x: (root.workspaceImplicitWidth + workspaceSpacing) * colIndex y: (root.workspaceImplicitHeight + workspaceSpacing) * rowIndex z: root.windowZ diff --git a/dots/.config/quickshell/ii/modules/ii/overview/SearchBar.qml b/dots/.config/quickshell/ii/modules/ii/overview/SearchBar.qml index 5cd7b9ac5..6a5de6a7e 100644 --- a/dots/.config/quickshell/ii/modules/ii/overview/SearchBar.qml +++ b/dots/.config/quickshell/ii/modules/ii/overview/SearchBar.qml @@ -81,7 +81,7 @@ RowLayout { } } - onTextChanged: root.searchingText = text + onTextChanged: LauncherSearch.query = text onAccepted: { if (appResults.count > 0) { diff --git a/dots/.config/quickshell/ii/modules/ii/overview/SearchItem.qml b/dots/.config/quickshell/ii/modules/ii/overview/SearchItem.qml index 8f72db56a..a0f9b248d 100644 --- a/dots/.config/quickshell/ii/modules/ii/overview/SearchItem.qml +++ b/dots/.config/quickshell/ii/modules/ii/overview/SearchItem.qml @@ -2,6 +2,7 @@ import qs import qs.services import qs.modules.common +import qs.modules.common.models import qs.modules.common.widgets import qs.modules.common.functions import QtQuick @@ -12,20 +13,27 @@ import Quickshell.Hyprland RippleButton { id: root - property var entry + property LauncherSearchResult entry property string query property bool entryShown: entry?.shown ?? true property string itemType: entry?.type ?? Translation.tr("App") property string itemName: entry?.name ?? "" - property string itemIcon: entry?.icon ?? "" + property var iconType: entry?.iconType + property string iconName: entry?.iconName ?? "" property var itemExecute: entry?.execute - property string fontType: entry?.fontType ?? "main" - property string itemClickActionName: entry?.clickActionName ?? "Open" - property string bigText: entry?.bigText ?? "" - property string materialSymbol: entry?.materialSymbol ?? "" - property string cliphistRawString: entry?.cliphistRawString ?? "" + property var fontType: switch(entry?.fontType) { + case LauncherSearchResult.FontType.Monospace: + return "monospace" + case LauncherSearchResult.FontType.Normal: + return "main" + default: + return "main" + } + property string itemClickActionName: entry?.verb ?? "Open" + property string bigText: entry?.iconType === LauncherSearchResult.IconType.Text ? entry?.iconName ?? "" : "" + property string materialSymbol: entry.iconType === LauncherSearchResult.IconType.Material ? entry?.iconName ?? "" : "" + property string cliphistRawString: entry?.rawValue ?? "" property bool blurImage: entry?.blurImage ?? false - property string blurImageText: entry?.blurImageText ?? "Image hidden" visible: root.entryShown property int horizontalMargin: 10 @@ -97,7 +105,7 @@ RippleButton { } Keys.onPressed: (event) => { if (event.key === Qt.Key_Delete && event.modifiers === Qt.ShiftModifier) { - const deleteAction = root.entry.actions.find(action => action.name == "Delete"); + const deleteAction = root.entry.actions.find(action => action.name == Translation.tr("Delete")); if (deleteAction) { deleteAction.execute() @@ -126,16 +134,24 @@ RippleButton { Loader { id: iconLoader active: true - sourceComponent: root.materialSymbol !== "" ? materialSymbolComponent : - root.bigText ? bigTextComponent : - root.itemIcon !== "" ? iconImageComponent : - null + sourceComponent: switch(root.iconType) { + case LauncherSearchResult.IconType.Material: + return materialSymbolComponent + case LauncherSearchResult.IconType.Text: + return bigTextComponent + case LauncherSearchResult.IconType.System: + return iconImageComponent + case LauncherSearchResult.IconType.None: + return null + default: + return null + } } Component { id: iconImageComponent IconImage { - source: Quickshell.iconPath(root.itemIcon, "image-missing") + source: Quickshell.iconPath(root.iconName, "image-missing") width: 35 height: 35 } @@ -217,7 +233,6 @@ RippleButton { maxWidth: contentColumn.width maxHeight: 140 blur: root.blurImage - blurText: root.blurImageText } } } @@ -243,8 +258,8 @@ RippleButton { delegate: RippleButton { id: actionButton required property var modelData - property string iconName: modelData.icon ?? "" - property string materialIconName: modelData.materialIcon ?? "" + property var iconType: modelData.iconType + property string iconName: modelData.iconName ?? "" implicitHeight: 34 implicitWidth: 34 @@ -256,16 +271,16 @@ RippleButton { anchors.centerIn: parent Loader { anchors.centerIn: parent - active: !(actionButton.iconName !== "") || actionButton.materialIconName + active: actionButton.iconType === LauncherSearchResult.IconType.Material || actionButton.iconName === "" sourceComponent: MaterialSymbol { - text: actionButton.materialIconName || "video_settings" + text: actionButton.iconName || "video_settings" font.pixelSize: Appearance.font.pixelSize.hugeass color: Appearance.m3colors.m3onSurface } } Loader { anchors.centerIn: parent - active: actionButton.materialIconName.length == 0 && actionButton.iconName && actionButton.iconName !== "" + active: actionButton.iconType === LauncherSearchResult.IconType.System && actionButton.iconName !== "" sourceComponent: IconImage { source: Quickshell.iconPath(actionButton.iconName) implicitSize: 20 diff --git a/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml b/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml index 03ebfce1b..3dd88aef3 100644 --- a/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml +++ b/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml @@ -14,80 +14,10 @@ import Quickshell.Io Item { // Wrapper id: root readonly property string xdgConfigHome: Directories.config - property string searchingText: "" + property string searchingText: LauncherSearch.query property bool showResults: searchingText != "" implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2 - implicitHeight: searchBar.implicitHeight + searchBar.verticalPadding * 2 + Appearance.sizes.elevationMargin * 2 - - property string mathResult: "" - property bool clipboardWorkSafetyActive: { - const enabled = Config.options.workSafety.enable.clipboard; - const sensitiveNetwork = (StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords)) - return enabled && sensitiveNetwork; - } - - property var searchActions: [ - { - action: "accentcolor", - execute: args => { - Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--noswitch", "--color", ...(args != '' ? [`${args}`] : [])]); - } - }, - { - action: "dark", - execute: () => { - Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "dark", "--noswitch"]); - } - }, - { - action: "konachanwallpaper", - execute: () => { - Quickshell.execDetached([Quickshell.shellPath("scripts/colors/random/random_konachan_wall.sh")]); - } - }, - { - action: "light", - execute: () => { - Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "light", "--noswitch"]); - } - }, - { - action: "superpaste", - execute: args => { - if (!/^(\d+)/.test(args.trim())) { // Invalid if doesn't start with numbers - Quickshell.execDetached([ - "notify-send", - Translation.tr("Superpaste"), - Translation.tr("Usage: %1superpaste NUM_OF_ENTRIES[i]\nSupply i when you want images\nExamples:\n%1superpaste 4i for the last 4 images\n%1superpaste 7 for the last 7 entries").arg(Config.options.search.prefix.action), - "-a", "Shell" - ]); - return; - } - const syntaxMatch = /^(?:(\d+)(i)?)/.exec(args.trim()); - const count = syntaxMatch[1] ? parseInt(syntaxMatch[1]) : 1; - const isImage = !!syntaxMatch[2]; - Cliphist.superpaste(count, isImage); - } - }, - { - action: "todo", - execute: args => { - Todo.addTask(args); - } - }, - { - action: "wallpaper", - execute: () => { - GlobalStates.wallpaperSelectorOpen = true; - } - }, - { - action: "wipeclipboard", - execute: () => { - Cliphist.wipe(); - } - }, - ] + implicitHeight: searchWidgetContent.implicitHeight + searchBar.verticalPadding * 2 + Appearance.sizes.elevationMargin * 2 function focusFirstItem() { appResults.currentIndex = 0; @@ -103,47 +33,13 @@ Item { // Wrapper function cancelSearch() { searchBar.searchInput.selectAll(); - root.searchingText = ""; + LauncherSearch.query = ""; searchBar.animateWidth = true; } function setSearchingText(text) { searchBar.searchInput.text = text; - root.searchingText = text; - } - - function containsUnsafeLink(entry) { - if (entry == undefined) return false; - const unsafeKeywords = Config.options.workSafety.triggerCondition.linkKeywords; - return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords); - } - - Timer { - id: nonAppResultsTimer - interval: Config.options.search.nonAppResultDelay - onTriggered: { - let expr = root.searchingText; - if (expr.startsWith(Config.options.search.prefix.math)) { - expr = expr.slice(Config.options.search.prefix.math.length); - } - mathProcess.calculateExpression(expr); - } - } - - Process { - id: mathProcess - property list baseCommand: ["qalc", "-t"] - function calculateExpression(expression) { - mathProcess.running = false; - mathProcess.command = baseCommand.concat(expression); - mathProcess.running = true; - } - stdout: SplitParser { - onRead: data => { - root.mathResult = data; - root.focusFirstItem(); - } - } + LauncherSearch.query = text; } Keys.onPressed: event => { @@ -285,167 +181,9 @@ Item { // Wrapper model: ScriptModel { id: model objectProp: "key" - values: { - // Search results are handled here - ////////////////// Skip? ////////////////// - if (root.searchingText == "") - return []; - - ///////////// Special cases /////////////// - if (root.searchingText.startsWith(Config.options.search.prefix.clipboard)) { - // Clipboard - const searchString = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.clipboard); - return Cliphist.fuzzyQuery(searchString).map((entry, index, array) => { - const mightBlurImage = Cliphist.entryIsImage(entry) && root.clipboardWorkSafetyActive; - let shouldBlurImage = mightBlurImage; - if (mightBlurImage) { - shouldBlurImage = shouldBlurImage && (containsUnsafeLink(array[index - 1]) || containsUnsafeLink(array[index + 1])); - } - const type = `#${entry.match(/^\s*(\S+)/)?.[1] || ""}` - return { - key: type, - cliphistRawString: entry, - name: StringUtils.cleanCliphistEntry(entry), - clickActionName: "", - type: type, - execute: () => { - Cliphist.copy(entry) - }, - actions: [ - { - name: "Copy", - materialIcon: "content_copy", - execute: () => { - Cliphist.copy(entry); - } - }, - { - name: "Delete", - materialIcon: "delete", - execute: () => { - Cliphist.deleteEntry(entry); - } - } - ], - blurImage: shouldBlurImage, - blurImageText: Translation.tr("Work safety") - }; - }).filter(Boolean); - } - else if (root.searchingText.startsWith(Config.options.search.prefix.emojis)) { - // Clipboard - const searchString = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.emojis); - return Emojis.fuzzyQuery(searchString).map(entry => { - const emoji = entry.match(/^\s*(\S+)/)?.[1] || "" - return { - key: emoji, - cliphistRawString: entry, - bigText: emoji, - name: entry.replace(/^\s*\S+\s+/, ""), - clickActionName: "", - type: "Emoji", - execute: () => { - Quickshell.clipboardText = entry.match(/^\s*(\S+)/)?.[1]; - } - }; - }).filter(Boolean); - } - - ////////////////// Init /////////////////// - nonAppResultsTimer.restart(); - const mathResultObject = { - key: `Math result: ${root.mathResult}`, - name: root.mathResult, - clickActionName: Translation.tr("Copy"), - type: Translation.tr("Math result"), - fontType: "monospace", - materialSymbol: 'calculate', - execute: () => { - Quickshell.clipboardText = root.mathResult; - } - }; - const appResultObjects = AppSearch.fuzzyQuery(StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.app)).map(entry => { - entry.clickActionName = Translation.tr("Launch"); - entry.type = Translation.tr("App"); - entry.key = entry.execute - return entry; - }) - const commandResultObject = { - key: `cmd ${root.searchingText}`, - name: StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.shellCommand).replace("file://", ""), - clickActionName: Translation.tr("Run"), - type: Translation.tr("Run command"), - fontType: "monospace", - materialSymbol: 'terminal', - execute: () => { - let cleanedCommand = root.searchingText.replace("file://", ""); - cleanedCommand = StringUtils.cleanPrefix(cleanedCommand, Config.options.search.prefix.shellCommand); - if (cleanedCommand.startsWith(Config.options.search.prefix.shellCommand)) { - cleanedCommand = cleanedCommand.slice(Config.options.search.prefix.shellCommand.length); - } - Quickshell.execDetached(["bash", "-c", searchingText.startsWith('sudo') ? `${Config.options.apps.terminal} fish -C '${cleanedCommand}'` : cleanedCommand]); - } - }; - const webSearchResultObject = { - key: `website ${root.searchingText}`, - name: StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.webSearch), - clickActionName: Translation.tr("Search"), - type: Translation.tr("Search the web"), - materialSymbol: 'travel_explore', - execute: () => { - let query = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.webSearch); - let url = Config.options.search.engineBaseUrl + query; - for (let site of Config.options.search.excludedSites) { - url += ` -site:${site}`; - } - Qt.openUrlExternally(url); - } - } - const launcherActionObjects = root.searchActions.map(action => { - const actionString = `${Config.options.search.prefix.action}${action.action}`; - if (actionString.startsWith(root.searchingText) || root.searchingText.startsWith(actionString)) { - return { - key: `Action ${actionString}`, - name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString, - clickActionName: Translation.tr("Run"), - type: Translation.tr("Action"), - materialSymbol: 'settings_suggest', - execute: () => { - action.execute(root.searchingText.split(" ").slice(1).join(" ")); - } - }; - } - return null; - }).filter(Boolean); - - //////// Prioritized by prefix ///////// - let result = []; - const startsWithNumber = /^\d/.test(root.searchingText); - const startsWithMathPrefix = root.searchingText.startsWith(Config.options.search.prefix.math); - const startsWithShellCommandPrefix = root.searchingText.startsWith(Config.options.search.prefix.shellCommand); - const startsWithWebSearchPrefix = root.searchingText.startsWith(Config.options.search.prefix.webSearch); - if (startsWithNumber || startsWithMathPrefix) { - result.push(mathResultObject); - } else if (startsWithShellCommandPrefix) { - result.push(commandResultObject); - } else if (startsWithWebSearchPrefix) { - result.push(webSearchResultObject); - } - - //////////////// Apps ////////////////// - result = result.concat(appResultObjects); - - ////////// Launcher actions //////////// - result = result.concat(launcherActionObjects); - - /// Math result, command, web search /// - if (Config.options.search.prefix.showDefaultActionsWithoutPrefix) { - if (!startsWithShellCommandPrefix) result.push(commandResultObject); - if (!startsWithNumber && !startsWithMathPrefix) result.push(mathResultObject); - if (!startsWithWebSearchPrefix) result.push(webSearchResultObject); - } - - return result; + values: LauncherSearch.results + onValuesChanged: { + root.focusFirstItem(); } } diff --git a/dots/.config/quickshell/ii/modules/ii/polkit/Polkit.qml b/dots/.config/quickshell/ii/modules/ii/polkit/Polkit.qml index e1c54a42f..50f04f675 100644 --- a/dots/.config/quickshell/ii/modules/ii/polkit/Polkit.qml +++ b/dots/.config/quickshell/ii/modules/ii/polkit/Polkit.qml @@ -6,37 +6,10 @@ import qs.modules.common.functions import QtQuick import Quickshell import Quickshell.Wayland -import Quickshell.Hyprland -Scope { +FullscreenPolkitWindow { id: root - - Loader { - active: PolkitService.active - sourceComponent: Variants { - model: Quickshell.screens - delegate: PanelWindow { - id: panelWindow - required property var modelData - screen: modelData - - anchors { - top: true - left: true - right: true - bottom: true - } - - color: "transparent" - WlrLayershell.namespace: "quickshell:polkit" - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - WlrLayershell.layer: WlrLayer.Overlay - exclusionMode: ExclusionMode.Ignore - - PolkitContent { - anchors.fill: parent - } - } - } + contentComponent: Component { + PolkitContent {} } } diff --git a/dots/.config/quickshell/ii/modules/ii/polkit/PolkitContent.qml b/dots/.config/quickshell/ii/modules/ii/polkit/PolkitContent.qml index baef7f0b5..7dfd045db 100644 --- a/dots/.config/quickshell/ii/modules/ii/polkit/PolkitContent.qml +++ b/dots/.config/quickshell/ii/modules/ii/polkit/PolkitContent.qml @@ -66,12 +66,7 @@ Item { WindowDialogParagraph { Layout.fillWidth: true horizontalAlignment: Text.AlignLeft - text: { - if (!PolkitService.flow) return; - return PolkitService.flow.message.endsWith(".") - ? PolkitService.flow.message.slice(0, -1) - : PolkitService.flow.message - } + text: PolkitService.cleanMessage } MaterialTextField { @@ -79,11 +74,7 @@ Item { Layout.fillWidth: true focus: true enabled: PolkitService.interactionAvailable - placeholderText: { - const inputPrompt = PolkitService.flow?.inputPrompt.trim() ?? ""; - const cleanedInputPrompt = inputPrompt.endsWith(":") ? inputPrompt.slice(0, -1) : inputPrompt; - return cleanedInputPrompt || (root.usePasswordChars ? Translation.tr("Password") : Translation.tr("Input")) - } + placeholderText: PolkitService.cleanPrompt echoMode: root.usePasswordChars ? TextInput.Password : TextInput.Normal onAccepted: root.submit(); @@ -95,7 +86,7 @@ Item { } WindowDialogButtonRow { - + Layout.bottomMargin: 10 // I honestly don't know why this is necessary Item { Layout.fillWidth: true } diff --git a/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml b/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml index a52471ba6..920e69b56 100644 --- a/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml +++ b/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml @@ -1,5 +1,6 @@ pragma ComponentBehavior: Bound import qs.modules.common +import qs.modules.common.utils import qs.modules.common.functions import qs.modules.common.widgets import qs.services @@ -26,20 +27,14 @@ PanelWindow { bottom: true } - // TODO: Ask: sidebar AI; Ocr: tesseract + // TODO: Ask: sidebar AI enum SnipAction { Copy, Edit, Search, CharRecognition, Record, RecordWithSound } enum SelectionMode { RectCorners, Circle } property var action: RegionSelection.SnipAction.Copy property var selectionMode: RegionSelection.SelectionMode.RectCorners signal dismiss() - - property string saveScreenshotDir: Config.options.screenSnip.savePath !== "" - ? Config.options.screenSnip.savePath - : "" property string screenshotDir: Directories.screenshotTemp - property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl - property string fileUploadApiEndpoint: "https://uguu.se/upload" property color overlayColor: "#88111111" property color brightText: Appearance.m3colors.darkmode ? Appearance.colors.colOnLayer0 : Appearance.colors.colLayer0 property color brightSecondary: Appearance.m3colors.darkmode ? Appearance.colors.colSecondary : Appearance.colors.colOnSecondary @@ -180,10 +175,12 @@ PanelWindow { property real regionX: Math.min(dragStartX, draggingX) property real regionY: Math.min(dragStartY, draggingY) - Process { + TempScreenshotProcess { id: screenshotProc running: true - command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(root.screen.name)}' '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`] + screen: root.screen + screenshotDir: root.screenshotDir + screenshotPath: root.screenshotPath onExited: (exitCode, exitStatus) => { if (root.enableContentRegions) imageDetectionProcess.running = true; root.preparationDone = !checkRecordingProc.running; @@ -229,6 +226,27 @@ PanelWindow { } } + function getScreenshotAction() { + switch(root.action) { + case RegionSelection.SnipAction.Copy: + return ScreenshotAction.Action.Copy; + case RegionSelection.SnipAction.Edit: + return ScreenshotAction.Action.Edit; + case RegionSelection.SnipAction.Search: + return ScreenshotAction.Action.Search; + case RegionSelection.SnipAction.CharRecognition: + return ScreenshotAction.Action.CharRecognition; + case RegionSelection.SnipAction.Record: + return ScreenshotAction.Action.Record; + case RegionSelection.SnipAction.RecordWithSound: + return ScreenshotAction.Action.RecordWithSound; + default: + console.warn("[Region Selector] Unknown snip action, skipping snip."); + root.dismiss(); + return; + } + } + function snip() { // Validity check if (root.regionWidth <= 0 || root.regionHeight <= 0) { @@ -246,61 +264,20 @@ PanelWindow { if (root.action === RegionSelection.SnipAction.Copy || root.action === RegionSelection.SnipAction.Edit) { root.action = root.mouseButton === Qt.RightButton ? RegionSelection.SnipAction.Edit : RegionSelection.SnipAction.Copy; } - - // Set command for action - const rx = Math.round(root.regionX * root.monitorScale); - const ry = Math.round(root.regionY * root.monitorScale); - const rw = Math.round(root.regionWidth * root.monitorScale); - const rh = Math.round(root.regionHeight * root.monitorScale); - const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(root.screenshotPath)} ` - + `-crop ${rw}x${rh}+${rx}+${ry}` - const cropToStdout = `${cropBase} -` - const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'` - const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'` - const slurpRegion = `${rx},${ry} ${rw}x${rh}` - const uploadAndGetUrl = (filePath) => { - return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'` - } - switch (root.action) { - case RegionSelection.SnipAction.Copy: - if (saveScreenshotDir === "") { - // not saving the screenshot, just copy to clipboard - snipProc.command = ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`] - break; - } - - const savePathBase = root.saveScreenshotDir - - snipProc.command = [ - "bash", "-c", - `mkdir -p '${StringUtils.shellSingleQuoteEscape(savePathBase)}' && \ - saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \ - savePath="${savePathBase}/$saveFileName" && \ - ${cropToStdout} | tee >(wl-copy) > "$savePath" && \ - ${cleanup}` - ] - - break; - case RegionSelection.SnipAction.Edit: - snipProc.command = ["bash", "-c", `${cropToStdout} | swappy -f - && ${cleanup}`] - break; - case RegionSelection.SnipAction.Search: - snipProc.command = ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(root.screenshotPath)})" && ${cleanup}`] - break; - case RegionSelection.SnipAction.CharRecognition: - snipProc.command = ["bash", "-c", `${cropInPlace} && tesseract '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}' stdout -l $(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/') | wl-copy && ${cleanup}`] - break; - case RegionSelection.SnipAction.Record: - snipProc.command = ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}'`] - break; - case RegionSelection.SnipAction.RecordWithSound: - snipProc.command = ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}' --sound`] - break; - default: - console.warn("[Region Selector] Unknown snip action, skipping snip."); - root.dismiss(); - return; - } + + const screenshotDir = Config.options.screenSnip.savePath !== "" ? // + Config.options.screenSnip.savePath : ""; + var screenshotAction = root.getScreenshotAction(); + const command = ScreenshotAction.getCommand( + root.regionX * root.monitorScale, // + root.regionY * root.monitorScale, // + root.regionWidth * root.monitorScale,// + root.regionHeight * root.monitorScale, // + root.screenshotPath, // + screenshotAction, // + screenshotDir + ) + snipProc.command = command; // Image post-processing snipProc.startDetached(); diff --git a/dots/.config/quickshell/ii/modules/ii/regionSelector/TargetRegion.qml b/dots/.config/quickshell/ii/modules/ii/regionSelector/TargetRegion.qml index a1ecbcd0f..d3763e0ab 100644 --- a/dots/.config/quickshell/ii/modules/ii/regionSelector/TargetRegion.qml +++ b/dots/.config/quickshell/ii/modules/ii/regionSelector/TargetRegion.qml @@ -25,6 +25,10 @@ Rectangle { border.width: targeted ? 4 : 2 radius: 4 + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + visible: opacity > 0 Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) diff --git a/dots/.config/quickshell/ii/modules/ii/sessionScreen/SessionScreen.qml b/dots/.config/quickshell/ii/modules/ii/sessionScreen/SessionScreen.qml index 8899c3e75..ee8714793 100644 --- a/dots/.config/quickshell/ii/modules/ii/sessionScreen/SessionScreen.qml +++ b/dots/.config/quickshell/ii/modules/ii/sessionScreen/SessionScreen.qml @@ -14,8 +14,259 @@ import Quickshell.Hyprland Scope { id: root property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) - property bool packageManagerRunning: false - property bool downloadRunning: false + + Loader { + id: sessionLoader + active: GlobalStates.sessionOpen + onActiveChanged: { + if (sessionLoader.active) + SessionWarnings.refresh(); + } + + Connections { + target: GlobalStates + function onScreenLockedChanged() { + if (GlobalStates.screenLocked) { + GlobalStates.sessionOpen = false; + } + } + } + + sourceComponent: PanelWindow { // Session menu + id: sessionRoot + visible: sessionLoader.active + property string subtitle + + function hide() { + GlobalStates.sessionOpen = false; + } + + exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell:session" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + color: ColorUtils.transparentize(Appearance.m3colors.m3background, Appearance.m3colors.darkmode ? 0.05 : 0.12) + + anchors { + top: true + left: true + right: true + } + + implicitWidth: root.focusedScreen?.width ?? 0 + implicitHeight: root.focusedScreen?.height ?? 0 + + MouseArea { + id: sessionMouseArea + anchors.fill: parent + onClicked: { + sessionRoot.hide(); + } + } + + ColumnLayout { // Content column + id: contentColumn + anchors.centerIn: parent + spacing: 15 + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + sessionRoot.hide(); + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 0 + StyledText { + // Title + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + font { + family: Appearance.font.family.title + pixelSize: Appearance.font.pixelSize.title + variableAxes: Appearance.font.variableAxes.title + } + text: Translation.tr("Session") + } + + StyledText { + // Small instruction + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Appearance.font.pixelSize.normal + text: Translation.tr("Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel") + } + } + + GridLayout { + columns: 4 + columnSpacing: 15 + rowSpacing: 15 + + SessionActionButton { + id: sessionLock + focus: sessionRoot.visible + buttonIcon: "lock" + buttonText: Translation.tr("Lock") + onClicked: { + Session.lock(); + sessionRoot.hide(); + } + onFocusChanged: { + if (focus) + sessionRoot.subtitle = buttonText; + } + KeyNavigation.right: sessionSleep + KeyNavigation.down: sessionHibernate + } + SessionActionButton { + id: sessionSleep + buttonIcon: "dark_mode" + buttonText: Translation.tr("Sleep") + onClicked: { + Session.suspend(); + sessionRoot.hide(); + } + onFocusChanged: { + if (focus) + sessionRoot.subtitle = buttonText; + } + KeyNavigation.left: sessionLock + KeyNavigation.right: sessionLogout + KeyNavigation.down: sessionShutdown + } + SessionActionButton { + id: sessionLogout + buttonIcon: "logout" + buttonText: Translation.tr("Logout") + onClicked: { + Session.logout(); + sessionRoot.hide(); + } + onFocusChanged: { + if (focus) + sessionRoot.subtitle = buttonText; + } + KeyNavigation.left: sessionSleep + KeyNavigation.right: sessionTaskManager + KeyNavigation.down: sessionReboot + } + SessionActionButton { + id: sessionTaskManager + buttonIcon: "browse_activity" + buttonText: Translation.tr("Task Manager") + onClicked: { + Session.launchTaskManager(); + sessionRoot.hide(); + } + onFocusChanged: { + if (focus) + sessionRoot.subtitle = buttonText; + } + KeyNavigation.left: sessionLogout + KeyNavigation.down: sessionFirmwareReboot + } + + SessionActionButton { + id: sessionHibernate + buttonIcon: "downloading" + buttonText: Translation.tr("Hibernate") + onClicked: { + Session.hibernate(); + sessionRoot.hide(); + } + onFocusChanged: { + if (focus) + sessionRoot.subtitle = buttonText; + } + KeyNavigation.up: sessionLock + KeyNavigation.right: sessionShutdown + } + SessionActionButton { + id: sessionShutdown + buttonIcon: "power_settings_new" + buttonText: Translation.tr("Shutdown") + onClicked: { + Session.poweroff(); + sessionRoot.hide(); + } + onFocusChanged: { + if (focus) + sessionRoot.subtitle = buttonText; + } + KeyNavigation.left: sessionHibernate + KeyNavigation.right: sessionReboot + KeyNavigation.up: sessionSleep + } + SessionActionButton { + id: sessionReboot + buttonIcon: "restart_alt" + buttonText: Translation.tr("Reboot") + onClicked: { + Session.reboot(); + sessionRoot.hide(); + } + onFocusChanged: { + if (focus) + sessionRoot.subtitle = buttonText; + } + KeyNavigation.left: sessionShutdown + KeyNavigation.right: sessionFirmwareReboot + KeyNavigation.up: sessionLogout + } + SessionActionButton { + id: sessionFirmwareReboot + buttonIcon: "settings_applications" + buttonText: Translation.tr("Reboot to firmware settings") + onClicked: { + Session.rebootToFirmware(); + sessionRoot.hide(); + } + onFocusChanged: { + if (focus) + sessionRoot.subtitle = buttonText; + } + KeyNavigation.up: sessionTaskManager + KeyNavigation.left: sessionReboot + } + } + + DescriptionLabel { + Layout.alignment: Qt.AlignHCenter + text: sessionRoot.subtitle + } + } + + RowLayout { + anchors { + top: contentColumn.bottom + topMargin: 10 + horizontalCenter: contentColumn.horizontalCenter + } + spacing: 10 + + Loader { + active: SessionWarnings.packageManagerRunning + visible: active + sourceComponent: DescriptionLabel { + text: Translation.tr("Your package manager is running") + textColor: Appearance.m3colors.m3onErrorContainer + color: Appearance.m3colors.m3errorContainer + } + } + Loader { + active: SessionWarnings.downloadRunning + visible: active + sourceComponent: DescriptionLabel { + text: Translation.tr("There might be a download in progress") + textColor: Appearance.m3colors.m3onErrorContainer + color: Appearance.m3colors.m3errorContainer + } + } + } + } + } component DescriptionLabel: Rectangle { id: descriptionLabel @@ -39,235 +290,6 @@ Scope { } } - function detectRunningStuff() { - packageManagerRunning = false; - downloadRunning = false; - detectPackageManagerProc.running = false; - detectPackageManagerProc.running = true; - detectDownloadProc.running = false; - detectDownloadProc.running = true; - } - - Process { - id: detectPackageManagerProc - command: ["bash", "-c", "pidof pacman yay paru dnf zypper apt apx xbps flatpak snap apk yum epsi pikman"] - onExited: (exitCode, exitStatus) => { - root.packageManagerRunning = (exitCode === 0); - } - } - - Process { - id: detectDownloadProc - command: ["bash", "-c", "pidof curl wget aria2c yt-dlp || ls ~/Downloads | grep -E '\.crdownload$|\.part$'"] - onExited: (exitCode, exitStatus) => { - root.downloadRunning = (exitCode === 0); - } - } - - Loader { - id: sessionLoader - active: GlobalStates.sessionOpen - onActiveChanged: { - if (sessionLoader.active) root.detectRunningStuff(); - } - - Connections { - target: GlobalStates - function onScreenLockedChanged() { - if (GlobalStates.screenLocked) { - GlobalStates.sessionOpen = false; - } - } - } - - sourceComponent: PanelWindow { // Session menu - id: sessionRoot - visible: sessionLoader.active - property string subtitle - - function hide() { - GlobalStates.sessionOpen = false; - } - - exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell:session" - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive - // This is a big surface so we needa carefully choose the transparency, - // or we'll get a large scary rgb blob - color: ColorUtils.transparentize(Appearance.m3colors.m3background, Appearance.m3colors.darkmode ? 0.04 : 0.12) - - anchors { - top: true - left: true - right: true - } - - implicitWidth: root.focusedScreen?.width ?? 0 - implicitHeight: root.focusedScreen?.height ?? 0 - - MouseArea { - id: sessionMouseArea - anchors.fill: parent - onClicked: { - sessionRoot.hide() - } - } - - ColumnLayout { // Content column - id: contentColumn - anchors.centerIn: parent - spacing: 15 - - Keys.onPressed: (event) => { - if (event.key === Qt.Key_Escape) { - sessionRoot.hide(); - } - } - - ColumnLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 0 - StyledText { // Title - Layout.alignment: Qt.AlignHCenter - horizontalAlignment: Text.AlignHCenter - font { - family: Appearance.font.family.title - pixelSize: Appearance.font.pixelSize.title - variableAxes: Appearance.font.variableAxes.title - } - text: Translation.tr("Session") - } - - StyledText { // Small instruction - Layout.alignment: Qt.AlignHCenter - horizontalAlignment: Text.AlignHCenter - font.pixelSize: Appearance.font.pixelSize.normal - text: Translation.tr("Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel") - } - } - - GridLayout { - columns: 4 - columnSpacing: 15 - rowSpacing: 15 - - SessionActionButton { - id: sessionLock - focus: sessionRoot.visible - buttonIcon: "lock" - buttonText: Translation.tr("Lock") - onClicked: { Session.lock(); sessionRoot.hide() } - onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } - KeyNavigation.right: sessionSleep - KeyNavigation.down: sessionHibernate - } - SessionActionButton { - id: sessionSleep - buttonIcon: "dark_mode" - buttonText: Translation.tr("Sleep") - onClicked: { Session.suspend(); sessionRoot.hide() } - onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } - KeyNavigation.left: sessionLock - KeyNavigation.right: sessionLogout - KeyNavigation.down: sessionShutdown - } - SessionActionButton { - id: sessionLogout - buttonIcon: "logout" - buttonText: Translation.tr("Logout") - onClicked: { Session.logout(); sessionRoot.hide() } - onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } - KeyNavigation.left: sessionSleep - KeyNavigation.right: sessionTaskManager - KeyNavigation.down: sessionReboot - } - SessionActionButton { - id: sessionTaskManager - buttonIcon: "browse_activity" - buttonText: Translation.tr("Task Manager") - onClicked: { Session.launchTaskManager(); sessionRoot.hide() } - onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } - KeyNavigation.left: sessionLogout - KeyNavigation.down: sessionFirmwareReboot - } - - SessionActionButton { - id: sessionHibernate - buttonIcon: "downloading" - buttonText: Translation.tr("Hibernate") - onClicked: { Session.hibernate(); sessionRoot.hide() } - onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } - KeyNavigation.up: sessionLock - KeyNavigation.right: sessionShutdown - } - SessionActionButton { - id: sessionShutdown - buttonIcon: "power_settings_new" - buttonText: Translation.tr("Shutdown") - onClicked: { Session.poweroff(); sessionRoot.hide() } - onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } - KeyNavigation.left: sessionHibernate - KeyNavigation.right: sessionReboot - KeyNavigation.up: sessionSleep - } - SessionActionButton { - id: sessionReboot - buttonIcon: "restart_alt" - buttonText: Translation.tr("Reboot") - onClicked: { Session.reboot(); sessionRoot.hide() } - onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } - KeyNavigation.left: sessionShutdown - KeyNavigation.right: sessionFirmwareReboot - KeyNavigation.up: sessionLogout - } - SessionActionButton { - id: sessionFirmwareReboot - buttonIcon: "settings_applications" - buttonText: Translation.tr("Reboot to firmware settings") - onClicked: { Session.rebootToFirmware(); sessionRoot.hide() } - onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } - KeyNavigation.up: sessionTaskManager - KeyNavigation.left: sessionReboot - } - } - - DescriptionLabel { - Layout.alignment: Qt.AlignHCenter - text: sessionRoot.subtitle - } - } - - RowLayout { - anchors { - top: contentColumn.bottom - topMargin: 10 - horizontalCenter: contentColumn.horizontalCenter - } - spacing: 10 - - Loader { - active: root.packageManagerRunning - visible: active - sourceComponent: DescriptionLabel { - text: Translation.tr("Your package manager is running") - textColor: Appearance.m3colors.m3onErrorContainer - color: Appearance.m3colors.m3errorContainer - } - } - Loader { - active: root.downloadRunning - visible: active - sourceComponent: DescriptionLabel { - text: Translation.tr("There might be a download in progress") - textColor: Appearance.m3colors.m3onErrorContainer - color: Appearance.m3colors.m3errorContainer - } - } - } - } - } - IpcHandler { target: "session" @@ -276,11 +298,11 @@ Scope { } function close(): void { - GlobalStates.sessionOpen = false + GlobalStates.sessionOpen = false; } function open(): void { - GlobalStates.sessionOpen = true + GlobalStates.sessionOpen = true; } } @@ -298,7 +320,7 @@ Scope { description: "Opens session screen on press" onPressed: { - GlobalStates.sessionOpen = true + GlobalStates.sessionOpen = true; } } @@ -307,8 +329,7 @@ Scope { description: "Closes session screen on press" onPressed: { - GlobalStates.sessionOpen = false + GlobalStates.sessionOpen = false; } } - } diff --git a/dots/.config/quickshell/ii/modules/ii/sidebarLeft/AiChat.qml b/dots/.config/quickshell/ii/modules/ii/sidebarLeft/AiChat.qml index 820626b51..3dc181fe4 100644 --- a/dots/.config/quickshell/ii/modules/ii/sidebarLeft/AiChat.qml +++ b/dots/.config/quickshell/ii/modules/ii/sidebarLeft/AiChat.qml @@ -314,7 +314,10 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) implicitWidth: statusRowLayout.implicitWidth + 10 * 2 implicitHeight: Math.max(statusRowLayout.implicitHeight, 38) radius: Appearance.rounding.normal - root.padding - color: Appearance.colors.colLayer2 + color: messageListView.atYBeginning ? Appearance.colors.colLayer2 : Appearance.colors.colLayer2Base + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } RowLayout { id: statusRowLayout anchors.centerIn: parent diff --git a/dots/.config/quickshell/ii/modules/ii/sidebarLeft/anime/BooruImage.qml b/dots/.config/quickshell/ii/modules/ii/sidebarLeft/anime/BooruImage.qml index e403417ba..feeeeb05b 100644 --- a/dots/.config/quickshell/ii/modules/ii/sidebarLeft/anime/BooruImage.qml +++ b/dots/.config/quickshell/ii/modules/ii/sidebarLeft/anime/BooruImage.qml @@ -126,7 +126,7 @@ Button { opacity: root.showActions ? 1 : 0 visible: opacity > 0 radius: Appearance.rounding.small - color: Appearance.colors.colSurfaceContainer + color: Appearance.m3colors.m3surfaceContainer implicitHeight: contextMenuColumnLayout.implicitHeight + radius * 2 implicitWidth: contextMenuColumnLayout.implicitWidth diff --git a/dots/.config/quickshell/ii/modules/ii/sidebarRight/BottomWidgetGroup.qml b/dots/.config/quickshell/ii/modules/ii/sidebarRight/BottomWidgetGroup.qml index f81e9db13..4c6ea6dd9 100644 --- a/dots/.config/quickshell/ii/modules/ii/sidebarRight/BottomWidgetGroup.qml +++ b/dots/.config/quickshell/ii/modules/ii/sidebarRight/BottomWidgetGroup.qml @@ -1,3 +1,4 @@ +pragma ComponentBehavior: Bound import qs.modules.common import qs.modules.common.widgets import qs.services @@ -12,32 +13,47 @@ Rectangle { radius: Appearance.rounding.normal color: Appearance.colors.colLayer1 clip: true - implicitHeight: collapsed ? collapsedBottomWidgetGroupRow.implicitHeight : bottomWidgetGroupRow.implicitHeight + implicitHeight: collapsed ? collapsedBottomWidgetGroupRow.implicitHeight : 350 property int selectedTab: Persistent.states.sidebar.bottomGroup.tab + property int previousIndex: -1 property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed property var tabs: [ - {"type": "calendar", "name": Translation.tr("Calendar"), "icon": "calendar_month", "widget": calendarWidget}, - {"type": "todo", "name": Translation.tr("To Do"), "icon": "done_outline", "widget": todoWidget}, - {"type": "timer", "name": Translation.tr("Timer"), "icon": "schedule", "widget": pomodoroWidget}, + { + "type": "calendar", + "name": Translation.tr("Calendar"), + "icon": "calendar_month", + "widget": "calendar/CalendarWidget.qml" + }, + { + "type": "todo", + "name": Translation.tr("To Do"), + "icon": "done_outline", + "widget": "todo/TodoWidget.qml" + }, + { + "type": "timer", + "name": Translation.tr("Timer"), + "icon": "schedule", + "widget": "pomodoro/PomodoroWidget.qml" + }, ] Behavior on implicitHeight { NumberAnimation { duration: Appearance.animation.elementMove.duration easing.type: Appearance.animation.elementMove.type - easing.bezierCurve: Appearance.animation.elementMove.bezierCurve + easing.bezierCurve: Appearance.animation.elementMove.bezierCurve } } function setCollapsed(state) { - Persistent.states.sidebar.bottomGroup.collapsed = state + Persistent.states.sidebar.bottomGroup.collapsed = state; if (collapsed) { - bottomWidgetGroupRow.opacity = 0 + bottomWidgetGroupRow.opacity = 0; + } else { + collapsedBottomWidgetGroupRow.opacity = 0; } - else { - collapsedBottomWidgetGroupRow.opacity = 0 - } - collapseCleanFadeTimer.start() + collapseCleanFadeTimer.start(); } Timer { @@ -45,18 +61,19 @@ Rectangle { interval: Appearance.animation.elementMove.duration / 2 repeat: false onTriggered: { - if(collapsed) collapsedBottomWidgetGroupRow.opacity = 1 - else bottomWidgetGroupRow.opacity = 1 + if (collapsed) + collapsedBottomWidgetGroupRow.opacity = 1; + else + bottomWidgetGroupRow.opacity = 1; } } - Keys.onPressed: (event) => { - if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) - && event.modifiers === Qt.ControlModifier) { + Keys.onPressed: event => { + if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.ControlModifier) { if (event.key === Qt.Key_PageDown) { - root.selectedTab = Math.min(root.selectedTab + 1, root.tabs.length - 1) + root.selectedTab = Math.min(root.selectedTab + 1, root.tabs.length - 1); } else if (event.key === Qt.Key_PageUp) { - root.selectedTab = Math.max(root.selectedTab - 1, 0) + root.selectedTab = Math.max(root.selectedTab - 1, 0); } event.accepted = true; } @@ -77,13 +94,13 @@ Rectangle { } spacing: 15 - + CalendarHeaderButton { Layout.margins: 10 Layout.rightMargin: 0 forceCircle: true downAction: () => { - root.setCollapsed(false) + root.setCollapsed(false); } contentItem: MaterialSymbol { text: "keyboard_arrow_up" @@ -94,7 +111,7 @@ Rectangle { } StyledText { - property int remainingTasks: Todo.list.filter(task => !task.done).length; + property int remainingTasks: Todo.list.filter(task => !task.done).length Layout.margins: 10 Layout.leftMargin: 0 // text: `${DateTime.collapsedCalendarFormat} • ${remainingTasks} task${remainingTasks > 1 ? "s" : ""}` @@ -119,10 +136,10 @@ Rectangle { } } - anchors.fill: parent - height: tabStack.height + anchors.fill: parent + // implicitHeight: tabStack.implicitHeight spacing: 10 - + // Navigation rail Item { Layout.fillHeight: true @@ -141,13 +158,15 @@ Rectangle { Repeater { model: root.tabs NavigationRailButton { + required property int index + required property var modelData showToggledHighlight: false toggled: root.selectedTab == index buttonText: modelData.name buttonIcon: modelData.icon onPressed: { - root.selectedTab = index - Persistent.states.sidebar.bottomGroup.tab = index + root.selectedTab = index; + Persistent.states.sidebar.bottomGroup.tab = index; } } } @@ -158,7 +177,7 @@ Rectangle { anchors.top: parent.top forceCircle: true downAction: () => { - root.setCollapsed(true) + root.setCollapsed(true); } contentItem: MaterialSymbol { text: "keyboard_arrow_down" @@ -170,83 +189,91 @@ Rectangle { } // Content area - StackLayout { - id: tabStack + Item { Layout.fillWidth: true - // Take the highest one, because the TODO list has no implicit height. This way the heigth of the calendar is used when it's initially loaded with the TODO list - height: Math.max(...tabStack.children.map(child => child.tabLoader?.implicitHeight || 0)) // TODO: make this less stupid - Layout.alignment: Qt.AlignVCenter - property int realIndex: root.selectedTab - property int animationDuration: Appearance.animation.elementMoveFast.duration * 1.5 - currentIndex: root.selectedTab + Layout.fillHeight: true + // implicitHeight: tabStack.implicitHeight - // Switch the tab on halfway of the anim duration - Connections { - target: root - function onSelectedTabChanged() { - delayedStackSwitch.start() - tabStack.realIndex = root.selectedTab - } - } - Timer { - id: delayedStackSwitch - interval: tabStack.animationDuration / 2 - repeat: false - onTriggered: { - tabStack.currentIndex = root.selectedTab - } - } + Loader { + id: tabStack + anchors.fill: parent + anchors.bottomMargin: -anchors.topMargin - Repeater { - model: tabs - Item { // TODO: make behavior on y also act for the item that's switched to - id: tabItem - property int tabIndex: index - property string tabType: modelData.type - property int animDistance: 5 - property var tabLoader: tabLoader - // Opacity: show up only when being animated to - opacity: (tabStack.currentIndex === tabItem.tabIndex && tabStack.realIndex === tabItem.tabIndex) ? 1 : 0 - // Y: starts animating when user selects a different tab - y: (tabStack.realIndex === tabItem.tabIndex) ? 0 : (tabStack.realIndex < tabItem.tabIndex) ? animDistance : -animDistance - Behavior on opacity { NumberAnimation { duration: tabStack.animationDuration / 2; easing.type: Easing.OutCubic } } - Behavior on y { NumberAnimation { duration: tabStack.animationDuration; easing.type: Easing.OutExpo } } - Loader { - id: tabLoader - anchors.fill: parent - sourceComponent: modelData.widget - focus: root.selectedTab === tabItem.tabIndex + Component.onCompleted: { + tabStack.source = root.tabs[root.selectedTab].widget; + } + + Connections { + target: root + function onSelectedTabChanged() { + if (root.currentTab > root.previousIndex) + tabSwitchBehavior.animation.down = true; + else if (root.currentTab < root.previousIndex) + tabSwitchBehavior.animation.down = false; + tabStack.source = root.tabs[root.selectedTab].widget; + } + } + + Behavior on source { + id: tabSwitchBehavior + animation: TabSwitchAnim { + id: upAnim + down: true } } } } } - // Calendar component - Component { - id: calendarWidget - - CalendarWidget { - anchors.fill: parent - anchors.margins: 5 + component TabSwitchAnim: SequentialAnimation { + id: switchAnim + property bool down: false + ParallelAnimation { + PropertyAnimation { + target: tabStack + properties: "opacity" + to: 0 + duration: Appearance.animation.elementMoveFast.duration + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve + } + PropertyAnimation { + target: tabStack.anchors + properties: "topMargin" + to: 10 * (switchAnim.down ? -1 : 1) + duration: Appearance.animation.elementMoveFast.duration + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve + } } - } - - // To Do component - Component { - id: todoWidget - TodoWidget { - anchors.fill: parent - anchors.margins: 5 + PropertyAction { + target: tabStack + property: "source" + value: root.tabs[root.selectedTab].widget + } // The source change happens here + ParallelAnimation { + PropertyAnimation { + target: tabStack.anchors + properties: "topMargin" + from: 10 * -(switchAnim.down ? -1 : 1) + to: 0 + duration: Appearance.animation.elementMoveFast.duration + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.animation.elementMoveEnter.bezierCurve + } + PropertyAnimation { + target: tabStack + properties: "opacity" + to: 1 + duration: Appearance.animation.elementMoveFast.duration + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.animation.elementMoveEnter.bezierCurve + } } - } - - // Pomodoro component - Component { - id: pomodoroWidget - PomodoroWidget { - anchors.fill: parent - anchors.margins: 5 + ScriptAction { + script: { + root.previousIndex = root.selectedTab; + } } } } diff --git a/dots/.config/quickshell/ii/modules/ii/sidebarRight/calendar/calendar_layout.js b/dots/.config/quickshell/ii/modules/ii/sidebarRight/calendar/calendar_layout.js index 7f750b411..e2042bff7 100644 --- a/dots/.config/quickshell/ii/modules/ii/sidebarRight/calendar/calendar_layout.js +++ b/dots/.config/quickshell/ii/modules/ii/sidebarRight/calendar/calendar_layout.js @@ -74,7 +74,7 @@ function getCalendarLayout(dateObject, highlight) { // Fill var monthDiff = (weekdayOfMonthFirst == 0 ? 0 : -1); var toFill, dim; - if(weekdayOfMonthFirst == 0) { + if (weekdayOfMonthFirst == 0) { toFill = 1; dim = daysInMonth; } @@ -88,8 +88,7 @@ function getCalendarLayout(dateObject, highlight) { calendar[i][j] = { "day": toFill, "today": ((toFill == day && monthDiff == 0 && highlight) ? 1 : ( - monthDiff == 0 ? 0 : - -1 + monthDiff == 0 ? 0 : -1 )) }; // Increment @@ -112,4 +111,3 @@ function getCalendarLayout(dateObject, highlight) { } return calendar; } - diff --git a/dots/.config/quickshell/ii/modules/ii/sidebarRight/todo/TaskList.qml b/dots/.config/quickshell/ii/modules/ii/sidebarRight/todo/TaskList.qml index 4f6a4304d..b48352f31 100644 --- a/dots/.config/quickshell/ii/modules/ii/sidebarRight/todo/TaskList.qml +++ b/dots/.config/quickshell/ii/modules/ii/sidebarRight/todo/TaskList.qml @@ -9,147 +9,108 @@ import Quickshell Item { id: root - required property var taskList; + required property var taskList property string emptyPlaceholderIcon property string emptyPlaceholderText property int todoListItemSpacing: 5 property int todoListItemPadding: 8 property int listBottomPadding: 80 - StyledFlickable { - id: flickable + StyledListView { + id: listView anchors.fill: parent - contentHeight: columnLayout.height - - clip: true - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: flickable.width - height: flickable.height - radius: Appearance.rounding.small - } + spacing: root.todoListItemSpacing + animateAppearance: false + model: ScriptModel { + values: root.taskList } + delegate: Item { + id: todoItem + required property var modelData + property bool pendingDoneToggle: false + property bool pendingDelete: false + property bool enableHeightAnimation: false - ColumnLayout { - id: columnLayout - width: parent.width - spacing: 0 - Repeater { - model: ScriptModel { - values: taskList + implicitHeight: todoItemRectangle.implicitHeight + width: ListView.view.width + clip: true + + Behavior on implicitHeight { + enabled: enableHeightAnimation + NumberAnimation { + duration: Appearance.animation.elementMoveFast.duration + easing.type: Appearance.animation.elementMoveFast.type + easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve } - delegate: Item { - id: todoItem - property bool pendingDoneToggle: false - property bool pendingDelete: false - property bool enableHeightAnimation: false - - Layout.fillWidth: true - implicitHeight: todoItemRectangle.implicitHeight + todoListItemSpacing - height: implicitHeight - clip: true - - Behavior on implicitHeight { - enabled: enableHeightAnimation - NumberAnimation { - duration: Appearance.animation.elementMoveFast.duration - easing.type: Appearance.animation.elementMoveFast.type - easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve - } - } - - function startAction() { - enableHeightAnimation = true - todoItem.implicitHeight = 0 - actionTimer.start() - } - - Timer { - id: actionTimer - interval: Appearance.animation.elementMoveFast.duration - repeat: false - onTriggered: { - if (todoItem.pendingDelete) { - Todo.deleteItem(modelData.originalIndex) - } else if (todoItem.pendingDoneToggle) { - if (!modelData.done) Todo.markDone(modelData.originalIndex) - else Todo.markUnfinished(modelData.originalIndex) - } - } - } - - Rectangle { - id: todoItemRectangle - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - implicitHeight: todoContentRowLayout.implicitHeight - color: Appearance.colors.colLayer2 - radius: Appearance.rounding.small - ColumnLayout { - id: todoContentRowLayout - anchors.left: parent.left - anchors.right: parent.right - - StyledText { - Layout.fillWidth: true // Needed for wrapping - Layout.leftMargin: 10 - Layout.rightMargin: 10 - Layout.topMargin: todoListItemPadding - id: todoContentText - text: modelData.content - wrapMode: Text.Wrap - } - RowLayout { - Layout.leftMargin: 10 - Layout.rightMargin: 10 - Layout.bottomMargin: todoListItemPadding - Item { - Layout.fillWidth: true - } - TodoItemActionButton { - Layout.fillWidth: false - onClicked: { - todoItem.pendingDoneToggle = true - todoItem.startAction() - } - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - text: modelData.done ? "remove_done" : "check" - iconSize: Appearance.font.pixelSize.larger - color: Appearance.colors.colOnLayer1 - } - } - TodoItemActionButton { - Layout.fillWidth: false - onClicked: { - todoItem.pendingDelete = true - todoItem.startAction() - } - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - text: "delete_forever" - iconSize: Appearance.font.pixelSize.larger - color: Appearance.colors.colOnLayer1 - } - } - } - } - } - } - } - // Bottom padding - Item { - implicitHeight: listBottomPadding + + Rectangle { + id: todoItemRectangle + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + implicitHeight: todoContentRowLayout.implicitHeight + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.small + + ColumnLayout { + id: todoContentRowLayout + anchors.left: parent.left + anchors.right: parent.right + + StyledText { + id: todoContentText + Layout.fillWidth: true // Needed for wrapping + Layout.leftMargin: 10 + Layout.rightMargin: 10 + Layout.topMargin: todoListItemPadding + text: todoItem.modelData.content + wrapMode: Text.Wrap + } + RowLayout { + Layout.leftMargin: 10 + Layout.rightMargin: 10 + Layout.bottomMargin: todoListItemPadding + Item { + Layout.fillWidth: true + } + TodoItemActionButton { + Layout.fillWidth: false + onClicked: { + if (!todoItem.modelData.done) + Todo.markDone(todoItem.modelData.originalIndex); + else + Todo.markUnfinished(todoItem.modelData.originalIndex); + } + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: todoItem.modelData.done ? "remove_done" : "check" + iconSize: Appearance.font.pixelSize.larger + color: Appearance.colors.colOnLayer1 + } + } + TodoItemActionButton { + Layout.fillWidth: false + onClicked: { + Todo.deleteItem(todoItem.modelData.originalIndex); + } + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: "delete_forever" + iconSize: Appearance.font.pixelSize.larger + color: Appearance.colors.colOnLayer1 + } + } + } + } } } } - - Item { // Placeholder when list is empty + + Item { + // Placeholder when list is empty visible: opacity > 0 opacity: taskList.length === 0 ? 1 : 0 anchors.fill: parent @@ -177,4 +138,4 @@ Item { } } } -} \ No newline at end of file +} diff --git a/dots/.config/quickshell/ii/modules/ii/sidebarRight/todo/TodoWidget.qml b/dots/.config/quickshell/ii/modules/ii/sidebarRight/todo/TodoWidget.qml index 36c885523..8341887fd 100644 --- a/dots/.config/quickshell/ii/modules/ii/sidebarRight/todo/TodoWidget.qml +++ b/dots/.config/quickshell/ii/modules/ii/sidebarRight/todo/TodoWidget.qml @@ -139,7 +139,7 @@ Item { anchors.margins: root.dialogMargins implicitHeight: dialogColumnLayout.implicitHeight - color: Appearance.colors.colSurfaceContainerHigh + color: Appearance.m3colors.m3surfaceContainerHigh radius: Appearance.rounding.normal function addTask() { diff --git a/dots/.config/quickshell/ii/modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml b/dots/.config/quickshell/ii/modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml index f7c2dc9f8..dab92df5f 100644 --- a/dots/.config/quickshell/ii/modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml +++ b/dots/.config/quickshell/ii/modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml @@ -30,6 +30,12 @@ ColumnLayout { required property var modelData node: modelData } + PagePlaceholder { + icon: "widgets" + title: Translation.tr("No applications") + shown: !root.hasApps + shape: MaterialShape.Shape.Cookie7Sided + } } StyledComboBox { diff --git a/dots/.config/quickshell/ii/modules/settings/BackgroundConfig.qml b/dots/.config/quickshell/ii/modules/settings/BackgroundConfig.qml index 66630cc24..b67e10409 100644 --- a/dots/.config/quickshell/ii/modules/settings/BackgroundConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/BackgroundConfig.qml @@ -53,9 +53,9 @@ ContentPage { } ContentSection { + id: settingsClock icon: "clock_loader_40" title: Translation.tr("Widget: Clock") - id: settingsClock function stylePresent(styleName) { if (!Config.options.background.widgets.clock.showOnlyWhenLocked && Config.options.background.widgets.clock.style === styleName) { @@ -120,61 +120,161 @@ ContentPage { } } - ContentSubsection { - visible: !Config.options.background.widgets.clock.showOnlyWhenLocked - title: Translation.tr("Clock style") - ConfigSelectionArray { - currentValue: Config.options.background.widgets.clock.style - onSelected: newValue => { - Config.options.background.widgets.clock.style = newValue; - } - options: [ - { - displayName: Translation.tr("Digital"), - icon: "timer_10", - value: "digital" - }, - { - displayName: Translation.tr("Cookie"), - icon: "cookie", - value: "cookie" + ConfigRow { + ContentSubsection { + visible: !Config.options.background.widgets.clock.showOnlyWhenLocked + title: Translation.tr("Clock style") + Layout.fillWidth: true + ConfigSelectionArray { + currentValue: Config.options.background.widgets.clock.style + onSelected: newValue => { + Config.options.background.widgets.clock.style = newValue; } - ] + options: [ + { + displayName: Translation.tr("Digital"), + icon: "timer_10", + value: "digital" + }, + { + displayName: Translation.tr("Cookie"), + icon: "cookie", + value: "cookie" + } + ] + } } - } - ContentSubsection { - title: Translation.tr("Clock style (locked)") - ConfigSelectionArray { - currentValue: Config.options.background.widgets.clock.styleLocked - onSelected: newValue => { - Config.options.background.widgets.clock.styleLocked = newValue; - } - options: [ - { - displayName: Translation.tr("Digital"), - icon: "timer_10", - value: "digital" - }, - { - displayName: Translation.tr("Cookie"), - icon: "cookie", - value: "cookie" + ContentSubsection { + title: Translation.tr("Clock style (locked)") + Layout.fillWidth: false + ConfigSelectionArray { + currentValue: Config.options.background.widgets.clock.styleLocked + onSelected: newValue => { + Config.options.background.widgets.clock.styleLocked = newValue; } - ] + options: [ + { + displayName: Translation.tr("Digital"), + icon: "timer_10", + value: "digital" + }, + { + displayName: Translation.tr("Cookie"), + icon: "cookie", + value: "cookie" + } + ] + } } } ContentSubsection { visible: settingsClock.digitalPresent title: Translation.tr("Digital clock settings") + tooltip: Translation.tr("Font width and roundness settings are only available for some fonts like Google Sans Flex") - ConfigSwitch { - buttonIcon: "animation" - text: Translation.tr("Animate time change") - checked: Config.options.background.widgets.clock.digital.animateChange - onCheckedChanged: { - Config.options.background.widgets.clock.digital.animateChange = checked; + ConfigRow { + uniform: true + ConfigSwitch { + buttonIcon: "vertical_distribute" + text: Translation.tr("Vertical") + checked: Config.options.background.widgets.clock.digital.vertical + onCheckedChanged: { + Config.options.background.widgets.clock.digital.vertical = checked; + } + } + ConfigSwitch { + buttonIcon: "animation" + text: Translation.tr("Animate time change") + checked: Config.options.background.widgets.clock.digital.animateChange + onCheckedChanged: { + Config.options.background.widgets.clock.digital.animateChange = checked; + } + } + } + + ConfigRow { + uniform: true + + ConfigSwitch { + buttonIcon: "date_range" + text: Translation.tr("Show date") + checked: Config.options.background.widgets.clock.digital.showDate + onCheckedChanged: { + Config.options.background.widgets.clock.digital.showDate = checked; + } + } + ConfigSwitch { + buttonIcon: "activity_zone" + text: Translation.tr("Use adaptive alignment") + checked: Config.options.background.widgets.clock.digital.adaptiveAlignment + onCheckedChanged: { + Config.options.background.widgets.clock.digital.adaptiveAlignment = checked; + } + StyledToolTip { + text: Translation.tr("Aligns the date and quote to left, center or right depending on its position on the screen.") + } + } + } + + MaterialTextArea { + Layout.fillWidth: true + placeholderText: Translation.tr("Font family") + text: Config.options.background.widgets.clock.digital.font.family + wrapMode: TextEdit.Wrap + onTextChanged: { + Config.options.background.widgets.clock.digital.font.family = text; + } + } + + ConfigSlider { + text: Translation.tr("Font weight") + value: Config.options.background.widgets.clock.digital.font.weight + usePercentTooltip: false + buttonIcon: "format_bold" + from: 1 + to: 1000 + stopIndicatorValues: [350] + onValueChanged: { + Config.options.background.widgets.clock.digital.font.weight = value; + } + } + + ConfigSlider { + text: Translation.tr("Font size") + value: Config.options.background.widgets.clock.digital.font.size + usePercentTooltip: false + buttonIcon: "format_size" + from: 70 + to: 150 + stopIndicatorValues: [90] + onValueChanged: { + Config.options.background.widgets.clock.digital.font.size = value; + } + } + + ConfigSlider { + text: Translation.tr("Font width") + value: Config.options.background.widgets.clock.digital.font.width + usePercentTooltip: false + buttonIcon: "fit_width" + from: 25 + to: 125 + stopIndicatorValues: [100] + onValueChanged: { + Config.options.background.widgets.clock.digital.font.width = value; + } + } + ConfigSlider { + text: Translation.tr("Font roundness") + value: Config.options.background.widgets.clock.digital.font.roundness + usePercentTooltip: false + buttonIcon: "line_curve" + from: 0 + to: 100 + onValueChanged: { + Config.options.background.widgets.clock.digital.font.roundness = value; } } } diff --git a/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml b/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml index b99e291b0..f94f1e811 100644 --- a/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml @@ -739,6 +739,45 @@ ContentPage { } } } + ConfigRow { + uniform: true + ConfigSelectionArray { + currentValue: Config.options.overview.orderRightLeft + onSelected: newValue => { + Config.options.overview.orderRightLeft = newValue + } + options: [ + { + displayName: Translation.tr("Left to right"), + icon: "arrow_forward", + value: 0 + }, + { + displayName: Translation.tr("Right to left"), + icon: "arrow_back", + value: 1 + } + ] + } + ConfigSelectionArray { + currentValue: Config.options.overview.orderBottomUp + onSelected: newValue => { + Config.options.overview.orderBottomUp = newValue + } + options: [ + { + displayName: Translation.tr("Top-down"), + icon: "arrow_downward", + value: 0 + }, + { + displayName: Translation.tr("Bottom-up"), + icon: "arrow_upward", + value: 1 + } + ] + } + } } ContentSection { diff --git a/dots/.config/quickshell/ii/modules/settings/QuickConfig.qml b/dots/.config/quickshell/ii/modules/settings/QuickConfig.qml index c9c540afa..8a31d7728 100644 --- a/dots/.config/quickshell/ii/modules/settings/QuickConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/QuickConfig.qml @@ -225,9 +225,6 @@ ContentPage { onCheckedChanged: { Config.options.appearance.transparency.enable = checked; } - StyledToolTip { - text: Translation.tr("Might look ass. Unsupported.") - } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/ActionCenterContent.qml b/dots/.config/quickshell/ii/modules/waffle/actionCenter/ActionCenterContent.qml index 4d45df92e..b69666660 100644 --- a/dots/.config/quickshell/ii/modules/waffle/actionCenter/ActionCenterContent.qml +++ b/dots/.config/quickshell/ii/modules/waffle/actionCenter/ActionCenterContent.qml @@ -1,3 +1,4 @@ +pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -12,28 +13,50 @@ import qs.modules.waffle.actionCenter.mainPage WBarAttachedPanelContent { id: root - contentItem: WStackView { - id: stackView - anchors.fill: parent - implicitWidth: initItem.implicitWidth - implicitHeight: initItem.implicitHeight - - initialItem: PageColumn { - id: initItem - MainPageBody {} - Separator {} - MainPageFooter {} + readonly property bool barAtBottom: Config.options.waffles.bar.bottom + + contentItem: ColumnLayout { + // This somewhat sophisticated anchoring is needed to make opening anim not jump abruptly when stuff appear + anchors { + left: parent.left + right: parent.right + top: root.barAtBottom ? undefined : parent.top + bottom: root.barAtBottom ? parent.bottom : undefined + margins: root.visualMargin + bottomMargin: 0 } + spacing: 12 - Component.onCompleted: { - ActionCenterContext.stackView = this + WPane { + opacity: (MprisController.activePlayer != null && MprisController.isRealPlayer(MprisController.activePlayer)) ? 1 : 0 + Layout.fillWidth: true + contentItem: MediaPaneContent {} } + WPane { + Layout.fillWidth: true + contentItem: WStackView { + id: stackView + implicitWidth: initItem.implicitWidth + implicitHeight: initItem.implicitHeight - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.BackButton - onClicked: { - ActionCenterContext.back() + initialItem: WPanelPageColumn { + id: initItem + MainPageBody {} + WPanelSeparator {} + MainPageFooter {} + } + + Component.onCompleted: { + ActionCenterContext.stackView = this; + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.BackButton + onClicked: { + ActionCenterContext.back(); + } + } } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/MediaPaneContent.qml b/dots/.config/quickshell/ii/modules/waffle/actionCenter/MediaPaneContent.qml new file mode 100644 index 000000000..5c4c6df97 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/actionCenter/MediaPaneContent.qml @@ -0,0 +1,156 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +Rectangle { + id: root + implicitHeight: 176 + implicitWidth: 358 + color: Looks.colors.bgPanelBody + anchors.fill: parent + + readonly property var activePlayer: MprisController.activePlayer + + Column { + anchors { + fill: parent + leftMargin: 23 + rightMargin: 23 + topMargin: 16 + bottomMargin: 20 + } + spacing: 25 + + AppInfoRow { + anchors { + left: parent.left + right: parent.right + } + } + + TrackInfoRow { + anchors { + left: parent.left + right: parent.right + } + } + + ControlButtonsRow { + anchors.horizontalCenter: parent.horizontalCenter + } + } + + component AppInfoRow: RowLayout { + id: appInfo + spacing: 8 + + property var desktopEntry: { + const desktopEntryString = root.activePlayer?.desktopEntry ?? ""; + return DesktopEntries.byId(desktopEntryString); + } + + FluentIcon { + implicitSize: 20 + icon: appInfo.desktopEntry?.icon || "music-note-2" + monochrome: !appInfo.desktopEntry?.icon + } + + WText { + Layout.fillWidth: true + text: appInfo.desktopEntry?.name ?? Translation.tr("Media") + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + } + } + + component TrackInfoRow: RowLayout { + spacing: 16 + + ColumnLayout { + id: trackInfo + Layout.fillWidth: true + spacing: 0 + + WText { + Layout.fillWidth: true + font.weight: Looks.font.weight.strong + font.pixelSize: Looks.font.pixelSize.large + elide: Text.ElideRight + text: StringUtils.cleanMusicTitle(root.activePlayer?.trackTitle) || Translation.tr("Unknown Title") + } + + WText { + Layout.fillWidth: true + elide: Text.ElideRight + text: root.activePlayer?.trackArtist || Translation.tr("Unknown Artist") + } + } + + StyledImage { + id: artImage + Layout.preferredWidth: 58 + Layout.preferredHeight: trackInfo.implicitHeight + source: MprisController.activeTrack?.artUrl || "" + fillMode: Image.PreserveAspectFit + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: artImage.width + height: artImage.height + Rectangle { + anchors.centerIn: parent + width: artImage.paintedWidth + height: artImage.paintedHeight + radius: Looks.radius.medium + } + } + } + } + } + + component ControlButtonsRow: RowLayout { + spacing: 26 + + MediaControlButton { + iconName: "previous" + enabled: root.activePlayer?.canGoPrevious ?? false + onClicked: root.activePlayer?.previous() + } + MediaControlButton { + readonly property bool playing: root.activePlayer?.isPlaying ?? false + iconName: playing ? "pause" : "play" + enabled: (playing && root.activePlayer?.canPause) || (!playing && root.activePlayer?.canPlay) + onClicked: root.activePlayer?.togglePlaying() + } + MediaControlButton { + iconName: "next" + enabled: root.activePlayer?.canGoNext ?? false + onClicked: root.activePlayer?.next() + } + } + + component MediaControlButton: WBorderlessButton { + id: controlButton + required property string iconName + implicitHeight: 40 + implicitWidth: 40 + + contentItem: Item { + FluentIcon { + anchors.centerIn: parent + icon: controlButton.iconName + monochrome: true + filled: true + implicitSize: 18 + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/WaffleActionCenter.qml b/dots/.config/quickshell/ii/modules/waffle/actionCenter/WaffleActionCenter.qml index 174b7851f..48e81bbc2 100644 --- a/dots/.config/quickshell/ii/modules/waffle/actionCenter/WaffleActionCenter.qml +++ b/dots/.config/quickshell/ii/modules/waffle/actionCenter/WaffleActionCenter.qml @@ -15,12 +15,13 @@ Scope { target: GlobalStates function onSidebarLeftOpenChanged() { - if (GlobalStates.sidebarLeftOpen) barLoader.active = true; + if (GlobalStates.sidebarLeftOpen) + panelLoader.active = true; } } Loader { - id: barLoader + id: panelLoader active: GlobalStates.sidebarLeftOpen sourceComponent: PanelWindow { id: panelWindow @@ -35,38 +36,31 @@ Scope { right: true } - implicitWidth: content.implicitWidth + content.visualMargin * 2 - implicitHeight: content.implicitHeight + content.visualMargin * 2 + implicitWidth: content.implicitWidth + implicitHeight: content.implicitHeight HyprlandFocusGrab { id: focusGrab active: true windows: [panelWindow] - onCleared: content.close(); + onCleared: content.close() } Connections { target: GlobalStates function onSidebarLeftOpenChanged() { - if (!GlobalStates.sidebarLeftOpen) content.close(); + if (!GlobalStates.sidebarLeftOpen) + content.close(); } } ActionCenterContent { id: content anchors.fill: parent - anchors.margins: visualMargin - - focus: true - Keys.onPressed: event => { // Esc to close - if (event.key === Qt.Key_Escape) { - content.close() - } - } onClosed: { GlobalStates.sidebarLeftOpen = false; - barLoader.active = false; + panelLoader.active = false; } } } @@ -88,6 +82,23 @@ Scope { name: "sidebarLeftToggle" description: "Toggles left sidebar on press" - onPressed: root.toggleOpen(); + onPressed: root.toggleOpen() + } + + IpcHandler { + target: "mediaControls" + + function toggle(): void { + GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen; + } + } + + GlobalShortcut { + name: "mediaControlsToggle" + description: "Toggles media controls on press" + + onPressed: { + GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen; + } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/bluetooth/BluetoothControl.qml b/dots/.config/quickshell/ii/modules/waffle/actionCenter/bluetooth/BluetoothControl.qml index c8d3a036a..05e89dafe 100644 --- a/dots/.config/quickshell/ii/modules/waffle/actionCenter/bluetooth/BluetoothControl.qml +++ b/dots/.config/quickshell/ii/modules/waffle/actionCenter/bluetooth/BluetoothControl.qml @@ -23,7 +23,7 @@ Item { Bluetooth.defaultAdapter.discovering = false; } - PageColumn { + WPanelPageColumn { anchors.fill: parent BodyRectangle { @@ -96,10 +96,10 @@ Item { } } - Separator {} + WPanelSeparator {} FooterRectangle { - FooterMoreButton { + WTextButton { anchors { verticalCenter: parent.verticalCenter left: parent.left @@ -110,7 +110,7 @@ Item { Quickshell.execDetached(["bash", "-c", Config.options.apps.bluetooth]); } } - WPanelFooterButton { + WBorderlessButton { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 12 diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/mainPage/MainPageBodyToggles.qml b/dots/.config/quickshell/ii/modules/waffle/actionCenter/mainPage/MainPageBodyToggles.qml index 88dbc9e03..64298b712 100644 --- a/dots/.config/quickshell/ii/modules/waffle/actionCenter/mainPage/MainPageBodyToggles.qml +++ b/dots/.config/quickshell/ii/modules/waffle/actionCenter/mainPage/MainPageBodyToggles.qml @@ -87,38 +87,16 @@ Item { } } - Column { + VerticalPageIndicator { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 6 - spacing: 6 - - NavigationArrow { - down: false - } - - Repeater { - model: root.pages - delegate: MouseArea { - id: pageIndicator - required property int index - hoverEnabled: true - onClicked: root.currentPage = index - anchors.horizontalCenter: parent.horizontalCenter - implicitWidth: 6 - implicitHeight: 6 - - Circle { - anchors.centerIn: parent - diameter: (index === root.currentPage || pageIndicator.containsMouse) && !pageIndicator.pressed ? 6 : 4 - color: pageIndicator.containsMouse ? Looks.colors.controlBgHover : Looks.colors.controlBg - } - } - } - - NavigationArrow { - down: true - } + + currentIndex: root.currentPage + count: root.pages + onClicked: (index) => root.currentPage = index + onIncreasePage: root.increasePage(); + onDecreasePage: root.decreasePage(); } FocusedScrollMouseArea { @@ -126,25 +104,7 @@ Item { anchors.fill: parent acceptedButtons: Qt.NoButton hoverEnabled: false - onScrollUp: decreasePage(); - onScrollDown: increasePage(); - } - - component NavigationArrow: FluentIcon { - id: navArrow - required property bool down - anchors.horizontalCenter: parent.horizontalCenter - implicitHeight: 12 - implicitWidth: 12 - (2 * upArea.containsPress) - icon: down ? "caret-down" : "caret-up" - color: upArea.containsMouse ? Looks.colors.controlBgHover : Looks.colors.controlBg - filled: true - opacity: ((down && root.currentPage < root.pages - 1) || (!down && root.currentPage > 0)) ? 1 : 0 - MouseArea { - id: upArea - anchors.fill: parent - hoverEnabled: true - onClicked: navArrow.down ? root.increasePage() : root.decreasePage(); - } + onScrollUp: root.decreasePage(); + onScrollDown: root.increasePage(); } } diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/mainPage/MainPageFooter.qml b/dots/.config/quickshell/ii/modules/waffle/actionCenter/mainPage/MainPageFooter.qml index 1190bd38e..3d5c8d882 100644 --- a/dots/.config/quickshell/ii/modules/waffle/actionCenter/mainPage/MainPageFooter.qml +++ b/dots/.config/quickshell/ii/modules/waffle/actionCenter/mainPage/MainPageFooter.qml @@ -11,7 +11,7 @@ import qs.modules.waffle.actionCenter FooterRectangle { // Battery button - WPanelFooterButton { + WBorderlessButton { visible: Battery.available anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left @@ -36,7 +36,7 @@ FooterRectangle { } // Settings button - WPanelFooterButton { + WBorderlessButton { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 12 diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/nightLight/NightLightControl.qml b/dots/.config/quickshell/ii/modules/waffle/actionCenter/nightLight/NightLightControl.qml index 71630295c..591e56399 100644 --- a/dots/.config/quickshell/ii/modules/waffle/actionCenter/nightLight/NightLightControl.qml +++ b/dots/.config/quickshell/ii/modules/waffle/actionCenter/nightLight/NightLightControl.qml @@ -24,7 +24,7 @@ Item { Bluetooth.defaultAdapter.discovering = false; } - PageColumn { + WPanelPageColumn { anchors.fill: parent BodyRectangle { @@ -61,7 +61,7 @@ Item { } } - Separator {} + WPanelSeparator {} FooterRectangle {} } diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/volumeControl/VolumeControl.qml b/dots/.config/quickshell/ii/modules/waffle/actionCenter/volumeControl/VolumeControl.qml index b3e97a78e..52fe9ab3d 100644 --- a/dots/.config/quickshell/ii/modules/waffle/actionCenter/volumeControl/VolumeControl.qml +++ b/dots/.config/quickshell/ii/modules/waffle/actionCenter/volumeControl/VolumeControl.qml @@ -14,7 +14,7 @@ Item { id: root property bool output: true - PageColumn { + WPanelPageColumn { anchors.fill: parent BodyRectangle { @@ -48,7 +48,7 @@ Item { } } - Separator {} + WPanelSeparator {} FooterRectangle { WButton { @@ -103,7 +103,7 @@ Item { } } - Separator { + WPanelSeparator { visible: EasyEffects.available && root.output color: Looks.colors.bg2Hover } @@ -129,7 +129,7 @@ Item { onClicked: EasyEffects.enable() } - Separator { + WPanelSeparator { color: Looks.colors.bg2Hover } diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/wifi/WifiControl.qml b/dots/.config/quickshell/ii/modules/waffle/actionCenter/wifi/WifiControl.qml index 6f07462be..061c74154 100644 --- a/dots/.config/quickshell/ii/modules/waffle/actionCenter/wifi/WifiControl.qml +++ b/dots/.config/quickshell/ii/modules/waffle/actionCenter/wifi/WifiControl.qml @@ -19,7 +19,7 @@ Item { Network.rescanWifi(); } - PageColumn { + WPanelPageColumn { anchors.fill: parent BodyRectangle { @@ -86,10 +86,10 @@ Item { } } - Separator {} + WPanelSeparator {} FooterRectangle { - FooterMoreButton { + WTextButton { anchors { verticalCenter: parent.verticalCenter left: parent.left @@ -100,7 +100,7 @@ Item { Quickshell.execDetached(["bash", "-c", Config.options.apps.network]); } } - WPanelFooterButton { + WBorderlessButton { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 12 diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/AppButton.qml b/dots/.config/quickshell/ii/modules/waffle/bar/AppButton.qml index 9d8d323fe..440695a2e 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/AppButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/AppButton.qml @@ -54,7 +54,7 @@ BarButton { contentItem: Item { id: contentItem - anchors.centerIn: background + anchors.centerIn: root.background implicitHeight: iconWidget.implicitHeight implicitWidth: iconWidget.implicitWidth @@ -66,7 +66,7 @@ BarButton { } } - AppIcon { + WAppIcon { id: iconWidget anchors.centerIn: parent iconName: root.iconName diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/BarButton.qml b/dots/.config/quickshell/ii/modules/waffle/bar/BarButton.qml index 53a37cba0..4502b22f7 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/BarButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/BarButton.qml @@ -5,17 +5,12 @@ import qs.modules.common import qs.modules.common.functions import qs.modules.waffle.looks -WButton { +AcrylicButton { id: root property var altAction: () => {} property var middleClickAction: () => {} - colBackground: ColorUtils.transparentize(Looks.colors.bg1) - colBackgroundHover: Looks.colors.bg1Hover - colBackgroundActive: Looks.colors.bg1Active - property color colBackgroundBorder - property color color Layout.fillHeight: true topInset: 4 bottomInset: 4 @@ -23,16 +18,7 @@ WButton { rightInset: 0 horizontalPadding: 8 - colBackgroundBorder: ColorUtils.transparentize(Looks.colors.bg1Border, (root.checked || root.hovered) ? Looks.backgroundTransparency : 1) - color: { - if (root.down) { - return root.colBackgroundActive - } else if ((root.hovered && !root.down) || root.checked) { - return root.colBackgroundHover - } else { - return root.colBackground - } - } + colBackground: ColorUtils.transparentize(Looks.colors.bg1) MouseArea { anchors.fill: parent @@ -50,15 +36,4 @@ WButton { } } - background: AcrylicRectangle { - shiny: ((root.hovered && !root.down) || root.checked) - color: root.color - radius: Looks.radius.medium - border.width: 1 - border.color: root.colBackgroundBorder - - Behavior on border.color { - animation: Looks.transition.color.createObject(this) - } - } } diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/SearchButton.qml b/dots/.config/quickshell/ii/modules/waffle/bar/SearchButton.qml index 3e8dd6282..6bb48f8fc 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/SearchButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/SearchButton.qml @@ -12,9 +12,9 @@ AppButton { iconName: checked ? "system-search-checked" : "system-search" separateLightDark: true - checked: GlobalStates.overviewOpen + checked: GlobalStates.searchOpen && LauncherSearch.query !== "" onClicked: { - GlobalStates.overviewOpen = !GlobalStates.overviewOpen; // For now... + GlobalStates.searchOpen = !GlobalStates.searchOpen; // For now... } BarToolTip { diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml b/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml index f4a15cc00..7fa716b07 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml @@ -7,14 +7,16 @@ import qs.services import qs.modules.common import qs.modules.waffle.looks +// TODO: Replace the icon with QMLized svg (with /usr/lib/qt6/bin/svgtoqml) for proper micro-animation AppButton { id: root leftInset: Config.options.waffles.bar.leftAlignApps ? 12 : 0 iconName: down ? "start-here-pressed" : "start-here" + checked: GlobalStates.searchOpen && LauncherSearch.query === "" onClicked: { - GlobalStates.overviewOpen = !GlobalStates.overviewOpen; // For now... + GlobalStates.searchOpen = !GlobalStates.searchOpen; } BarToolTip { diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/WaffleBar.qml b/dots/.config/quickshell/ii/modules/waffle/bar/WaffleBar.qml index 2326a47f7..b69b7c8e2 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/WaffleBar.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/WaffleBar.qml @@ -13,7 +13,7 @@ Scope { LazyLoader { id: barLoader - active: GlobalStates.barOpen && !GlobalStates.screenLocked + active: GlobalStates.barOpen component: Variants { model: Quickshell.screens delegate: PanelWindow { // Bar window diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/WidgetsButton.qml b/dots/.config/quickshell/ii/modules/waffle/bar/WidgetsButton.qml index 51a3175bc..c1c16096b 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/WidgetsButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/WidgetsButton.qml @@ -42,7 +42,7 @@ AppButton { } spacing: 6 - AppIcon { + WAppIcon { id: iconWidget anchors.verticalCenter: parent.verticalCenter iconName: root.iconName diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml b/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml index f33c6e12a..a2670118e 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml @@ -9,8 +9,8 @@ MouseArea { id: root Layout.fillHeight: true - implicitHeight: row.implicitHeight - implicitWidth: row.implicitWidth + implicitHeight: appRow.implicitHeight + implicitWidth: appRow.implicitWidth hoverEnabled: true function showPreviewPopup(appEntry, button) { @@ -21,31 +21,31 @@ MouseArea { animation: Looks.transition.move.createObject(this) } - // Apps row - RowLayout { - id: row + WListView { + id: appRow anchors { top: parent.top bottom: parent.bottom } + orientation: Qt.Horizontal spacing: 0 + implicitWidth: contentWidth + clip: true + interactive: false + // TODO: Include only apps (and windows) in current workspace only | wait, does that even make sense in a Hyprland workflow? + model: ScriptModel { + objectProp: "appId" + values: TaskbarApps.apps.filter(app => app.appId !== "SEPARATOR") + } + delegate: TaskAppButton { + required property var modelData + appEntry: modelData - Repeater { - // TODO: Include only apps (and windows) in current workspace only | wait, does that even make sense in a Hyprland workflow? - model: ScriptModel { - objectProp: "appId" - values: TaskbarApps.apps.filter(app => app.appId !== "SEPARATOR") + onHoverPreviewRequested: { + root.showPreviewPopup(appEntry, this); } - delegate: TaskAppButton { - required property var modelData - appEntry: modelData - - onHoverPreviewRequested: { - root.showPreviewPopup(appEntry, this) - } - onHoverPreviewDismissed: { - previewPopup.close() - } + onHoverPreviewDismissed: { + previewPopup.close(); } } } @@ -56,5 +56,4 @@ MouseArea { tasksHovered: root.containsMouse anchor.window: root.QsWindow.window } - } diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/tasks/WindowPreview.qml b/dots/.config/quickshell/ii/modules/waffle/bar/tasks/WindowPreview.qml index 2839a6747..764d91ca7 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/tasks/WindowPreview.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/tasks/WindowPreview.qml @@ -43,7 +43,7 @@ Button { Layout.fillHeight: false spacing: 8 - AppIcon { + WAppIcon { id: appIcon Layout.leftMargin: Looks.radius.large - root.padding + 2 Layout.alignment: Qt.AlignVCenter @@ -67,7 +67,7 @@ Button { } } - CloseButton { + WindowCloseButton { id: closeButton } } @@ -91,46 +91,14 @@ Button { } } - component CloseButton: Button { - id: reusableCloseButton + component WindowCloseButton: CloseButton { visible: root.hovered Layout.leftMargin: 4 implicitHeight: 30 implicitWidth: 30 + radius: Looks.radius.large - root.padding onClicked: { root.toplevel.close(); } - - Rectangle { - z: 0 - color: "transparent" - anchors.fill: closeButtonBg - anchors.margins: -1 - opacity: closeButtonBg.opacity - border.width: 1 - radius: closeButtonBg.radius + 1 - border.color: Looks.colors.bg2Border - } - - background: Rectangle { - id: closeButtonBg - z: 1 - opacity: reusableCloseButton.hovered ? 1 : 0 - radius: Looks.radius.large - root.padding - color: reusableCloseButton.pressed ? Looks.colors.dangerActive : Looks.colors.danger - Behavior on opacity { - animation: Looks.transition.opacity.createObject(this) - } - Behavior on color { - animation: Looks.transition.color.createObject(this) - } - } - - contentItem: FluentIcon { - z: 2 - anchors.centerIn: parent - icon: "dismiss" - implicitSize: 10 - } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/lock/WaffleLock.qml b/dots/.config/quickshell/ii/modules/waffle/lock/WaffleLock.qml new file mode 100644 index 000000000..b1ff6f353 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/lock/WaffleLock.qml @@ -0,0 +1,382 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.common.panels.lock +import qs.modules.waffle.looks +import qs.modules.waffle.sessionScreen as SessionScreen + +LockScreen { + id: root + + property bool passwordView: false + + lockSurface: Item { + id: lockSurfaceItem + + Component.onCompleted: { + root.passwordView = false; + lockSurfaceItem.forceActiveFocus(); + } + + Keys.onPressed: { + interactables.switchToFocusedView(); + } + + Image { + id: bg + z: 0 + width: parent.width + height: parent.height + onStatusChanged: { + if (status === Image.Ready) { + print("Lock wallpaper loaded"); + print(lockSurfaceItem.height); + y = -lockSurfaceItem.height; + openAnim.restart(); + } + } + sourceSize: Qt.size(lockSurfaceItem.width, lockSurfaceItem.height) + source: Config.options.background.wallpaperPath + fillMode: Image.PreserveAspectCrop + + PropertyAnimation { + id: openAnim + target: bg + property: "y" + to: 0 + duration: 350 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + } + + GaussianBlur { + z: 1 + anchors.fill: bg + source: bg + radius: 100 + samples: radius * 2 + 1 + scale: root.passwordView ? 1.1 : 1 + opacity: root.passwordView ? 1 : 0 + + Behavior on opacity { + animation: Looks.transition.opacity.createObject(this) + } + + Behavior on scale { + NumberAnimation { + duration: 400 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + } + } + + Interactables { + id: interactables + z: 2 + anchors.fill: bg + } + } + + component Interactables: Rectangle { + id: interactablesComponent + color: ColorUtils.transparentize("#000000", 0.8) + // Button { + // onClicked: { + // root.context.unlocked(LockContext.ActionEnum.Unlock); + // GlobalStates.screenLocked = false; + // } + // text: "woah it doesnt work let me out pls uwu colon three" + // } + + function switchToFocusedView() { + switchToPasswordViewAnim.restart(); + } + + SequentialAnimation { + id: switchToPasswordViewAnim + PropertyAnimation { + target: unfocusedContent + property: "y" + from: 0 + to: -height * 1.1 + duration: 250 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + ScriptAction { + script: { + root.passwordView = true; + } + } + } + + Item { + id: unfocusedContent + width: parent.width + height: parent.height + visible: !root.passwordView + ClockTextGroup { + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: interactablesComponent.height * 0.1 + } + } + RowLayout { + anchors { + bottom: parent.bottom + right: parent.right + bottomMargin: 21 + rightMargin: 31 + } + IconIndicator { + baseIcon: "wifi-1" + icon: WIcons.internetIcon + } + IconIndicator { + baseIcon: WIcons.batteryIcon + icon: WIcons.batteryLevelIcon + } + } + } + + Item { + id: focusedContent + anchors.fill: parent + visible: root.passwordView + + PasswordGroup { + visible: root.passwordView + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + } + + RowLayout { + visible: root.passwordView + anchors { + bottom: parent.bottom + right: parent.right + bottomMargin: 21 + rightMargin: 31 + } + SessionScreen.PowerButton { + id: powerButton + } + } + } + } + + component IconIndicator: Item { + id: iconIndicator + required property string baseIcon + required property string icon + default property alias data: iconWidget.data + implicitWidth: 40 + implicitHeight: 40 + FluentIcon { + id: iconWidget + anchors.centerIn: parent + icon: iconIndicator.baseIcon + color: Looks.darkColors.inactiveIcon + implicitSize: 20 + FluentIcon { + anchors.fill: parent + icon: iconIndicator.icon + } + } + } + + component ClockTextGroup: Column { + id: clockTextGroup + spacing: -3 + + WText { + anchors.horizontalCenter: parent.horizontalCenter + color: Looks.darkColors.fg + font.pixelSize: 133 + font.weight: Looks.font.weight.strong + text: { + // Don't take am/pm + // Match groups of digits separated by non-digit chars (e.g., "12:34", "12.34", "12-34") + let match = DateTime.time.match(/(\d{1,2})\D+(\d{2})/); + return match ? `${match[1]}${DateTime.time.match(/\D+/)[0]}${match[2]}` : DateTime.time; + } + } + + WText { + id: dateLabel + color: Looks.darkColors.fg + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: 28 + font.weight: Looks.font.weight.strong + text: DateTime.collapsedCalendarFormat + } + } + + component PasswordGroup: ColumnLayout { + id: passwordGroup + spacing: 15 + + WUserAvatar { + Layout.alignment: Qt.AlignHCenter + sourceSize: Qt.size(192, 192) + } + + WText { + Layout.alignment: Qt.AlignHCenter + text: SystemInfo.username + color: Looks.darkColors.fg + font.pixelSize: 26 + font.weight: Looks.font.weight.strong + } + + Rectangle { + id: passwordInputWrapper + Layout.topMargin: 10 + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: 132 + color: "transparent" + implicitWidth: 296 + implicitHeight: 36 + border.width: 2 + border.color: Looks.applyContentTransparency(Looks.darkColors.bg1Border) + radius: Looks.radius.medium + + Rectangle { + id: passwordInputBackground + anchors.fill: parent + anchors.margins: 2 + radius: Looks.radius.small + 1 + color: passwordInput.focus ? Looks.applyBackgroundTransparency(Looks.darkColors.bg1Base) : Looks.applyContentTransparency(Looks.darkColors.bg1) + + RowLayout { + anchors.fill: parent + anchors.margins: 6 + spacing: 3 + + WTextInput { + id: passwordInput + Layout.fillHeight: true + Layout.fillWidth: true + verticalAlignment: TextInput.AlignVCenter + inputMethodHints: Qt.ImhSensitiveData + echoMode: passwordVisibilityButton.pressed ? TextInput.Normal : TextInput.Password + color: Looks.darkColors.fg + + font.pixelSize: 12 + WText { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + visible: passwordInput.text.length === 0 + text: Translation.tr("Password") + font.pixelSize: Looks.font.pixelSize.large + color: Looks.darkColors.fg + opacity: 0.8 + } + + onTextChanged: root.context.currentText = this.text + onAccepted: { + root.context.tryUnlock(); + } + Connections { + target: root.context + function onCurrentTextChanged() { + passwordInput.text = root.context.currentText; + } + } + Connections { + target: root + function onPasswordViewChanged() { + passwordInput.forceActiveFocus(); + } + } + + Keys.onPressed: event => { + root.context.resetClearTimer(); + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: Qt.IBeamCursor + } + } + + PasswordBoxButton { + id: passwordVisibilityButton + property bool passwordVisible: false + visible: passwordInput.text.length > 0 + onPressed: passwordVisible = true + onReleased: passwordVisible = false + icon.name: passwordVisible ? "eye-off" : "eye" + } + + PasswordBoxButton { + onClicked: { + root.context.tryUnlock(); + } + icon.name: "arrow-right" + } + } + } + Rectangle { + id: activeIndicatorLine + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + implicitHeight: 2 + color: passwordInput.focus ? Looks.colors.accent : Looks.applyContentTransparency(Looks.darkColors.bg2Border) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: passwordInputWrapper.width + height: passwordInputWrapper.height + radius: passwordInputWrapper.radius + } + } + } + + Item {} + } + + component PasswordBoxButton: WButton { + id: pwBoxBtn + implicitWidth: 28 + implicitHeight: 22 + + property color colBackground: ColorUtils.transparentize(Looks.darkColors.bg1) + property color colBackgroundHover: ColorUtils.transparentize(Looks.darkColors.bg2Hover) + property color colBackgroundActive: ColorUtils.transparentize(Looks.darkColors.bg2Active) + fgColor: checked ? Looks.colors.accentFg : Looks.darkColors.fg + + checked: hovered + + contentItem: Item { + FluentIcon { + color: pwBoxBtn.fgColor + anchors.centerIn: parent + icon: pwBoxBtn.icon.name + implicitSize: 16 + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/AcrylicButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/AcrylicButton.qml new file mode 100644 index 000000000..ef5c0747a --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/AcrylicButton.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +WButton { + id: root + + colBackground: Looks.colors.bg1 + colBackgroundHover: Looks.colors.bg1Hover + colBackgroundActive: Looks.colors.bg1Active + property color colBackgroundBorder + property color color + property alias border: background.border + property alias shinyColor: background.borderColor + + colBackgroundBorder: ColorUtils.transparentize(color, (root.checked || root.hovered) ? Looks.backgroundTransparency : 0) + color: { + if (root.down) { + return root.colBackgroundActive + } else if ((root.hovered && !root.down) || root.checked) { + return root.colBackgroundHover + } else { + return root.colBackground + } + } + + background: AcrylicRectangle { + id: background + shiny: ((root.hovered && !root.down) || root.checked) + color: root.color + radius: Looks.radius.medium + border.width: 1 + border.color: root.colBackgroundBorder + + Behavior on border.color { + animation: Looks.transition.color.createObject(this) + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/AcrylicRectangle.qml b/dots/.config/quickshell/ii/modules/waffle/looks/AcrylicRectangle.qml index 7e041d111..7fecaa068 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/AcrylicRectangle.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/AcrylicRectangle.qml @@ -9,16 +9,17 @@ Rectangle { id: root property bool shiny: true // Top border - property color borderColor: ColorUtils.transparentize(Looks.colors.bg2Border, shiny ? 0.5 : 1) + property color borderColor: ColorUtils.transparentize(Looks.colors.bg1Hover, 0.7) + property color internalBorderColor: ColorUtils.transparentize(borderColor, shiny ? 0.0 : 1) color: Looks.colors.bg1Hover radius: Looks.radius.medium Behavior on color { animation: Looks.transition.color.createObject(this) } - Behavior on borderColor { + Behavior on internalBorderColor { animation: Looks.transition.color.createObject(this) } - onBorderColorChanged: { + onInternalBorderColorChanged: { borderCanvas.requestPaint(); } @@ -32,7 +33,7 @@ Rectangle { var ctx = getContext("2d"); ctx.clearRect(0, 0, width, height); - var borderColor = root.borderColor; + var borderColor = root.internalBorderColor; var r = root.radius; var fadeLength = Math.max(1, r); diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/BodyRectangle.qml b/dots/.config/quickshell/ii/modules/waffle/looks/BodyRectangle.qml similarity index 100% rename from dots/.config/quickshell/ii/modules/waffle/actionCenter/BodyRectangle.qml rename to dots/.config/quickshell/ii/modules/waffle/looks/BodyRectangle.qml diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/CloseButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/CloseButton.qml new file mode 100644 index 000000000..3345d0cc8 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/CloseButton.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks +import qs.modules.waffle.bar +import Quickshell + +Button { + id: reusableCloseButton + implicitHeight: 30 + implicitWidth: 30 + property alias radius: closeButtonBg.radius + + Rectangle { + z: 0 + color: "transparent" + anchors.fill: closeButtonBg + anchors.margins: -1 + opacity: closeButtonBg.opacity + border.width: 1 + radius: closeButtonBg.radius + 1 + border.color: Looks.colors.bg2Border + } + + background: Rectangle { + id: closeButtonBg + z: 1 + opacity: reusableCloseButton.hovered ? 1 : 0 + color: reusableCloseButton.pressed ? Looks.colors.dangerActive : Looks.colors.danger + Behavior on opacity { + animation: Looks.transition.opacity.createObject(this) + } + Behavior on color { + animation: Looks.transition.color.createObject(this) + } + } + + contentItem: FluentIcon { + z: 2 + anchors.centerIn: parent + icon: "dismiss" + implicitSize: 10 + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/FooterRectangle.qml b/dots/.config/quickshell/ii/modules/waffle/looks/FooterRectangle.qml similarity index 92% rename from dots/.config/quickshell/ii/modules/waffle/actionCenter/FooterRectangle.qml rename to dots/.config/quickshell/ii/modules/waffle/looks/FooterRectangle.qml index e3f7cd120..dcf4f519d 100644 --- a/dots/.config/quickshell/ii/modules/waffle/actionCenter/FooterRectangle.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/FooterRectangle.qml @@ -12,6 +12,6 @@ Rectangle { Layout.fillWidth: true color: "transparent" - implicitWidth: 360 + implicitWidth: 358 implicitHeight: 47 } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml b/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml index 26ef9f101..7bad59e84 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml @@ -17,33 +17,33 @@ Singleton { property string iconsPath: `${Directories.assetsPath}/icons/fluent` property bool dark: Appearance.m3colors.darkmode - property real backgroundTransparency: 0.13 - property real panelBackgroundTransparency: 0.12 + readonly property bool transparencyEnabled: Config.options.appearance.transparency.enable + property real backgroundTransparency: transparencyEnabled ? 0.16 : 0 + property real panelBackgroundTransparency: transparencyEnabled ? 0.14 : 0 property real panelLayerTransparency: root.dark ? 0.9 : 0.7 - property real contentTransparency: root.dark ? 0.9 : 0.5 + property real contentTransparency: root.dark ? 0.87 : 0.5 function applyBackgroundTransparency(col) { return ColorUtils.applyAlpha(col, 1 - root.backgroundTransparency) } function applyContentTransparency(col) { return ColorUtils.applyAlpha(col, 1 - root.contentTransparency) } - lightColors: QtObject { // TODO: figure out transparency + lightColors: QtObject { id: lightColors - property color bgPanelFooter: "#EEEEEE" property color bgPanelBody: "#F2F2F2" property color bgPanelSeparator: "#E0E0E0" property color bg0: "#EEEEEE" - property color bg0Border: '#adadad' - property color bg1: "#F7F7F7" + property color bg0Border: '#BEBEBE' property color bg1Base: "#F7F7F7" + property color bg1: "#F7F7F7" property color bg1Hover: "#F7F7F7" property color bg1Active: '#EFEFEF' - property color bg1Border: '#d7d7d7' + property color bg1Border: '#E9E9E9' property color bg2: "#FBFBFB" property color bg2Base: "#FBFBFB" - property color bg2Hover: "#FDFDFD" - property color bg2Active: "#FDFDFD" - property color bg2Border: '#cdcdcd' + property color bg2Hover: '#ffffff' + property color bg2Active: '#eeeeee' + property color bg2Border: '#E0E0E0' property color subfg: "#5C5C5C" property color fg: "#000000" property color fg1: "#626262" @@ -53,24 +53,25 @@ Singleton { property color controlBgHover: '#57575B' property color controlFg: "#FFFFFF" property color accentUnfocused: "#848484" + property color link: "#235CCF" + property color inputBg: ColorUtils.transparentize(bg0, 0.4) } darkColors: QtObject { id: darkColors - property color bgPanelFooter: "#1C1C1C" - property color bgPanelBody: '#616161' + property color bgPanelBody: '#242424' property color bgPanelSeparator: "#191919" property color bg0: "#1C1C1C" property color bg0Border: "#404040" - property color bg1Base: "#2C2C2C" - property color bg1: "#a8a8a8" - property color bg1Hover: "#b3b3b3" - property color bg1Active: '#727272' + property color bg1Base: '#2C2C2C' + property color bg1: '#2C2C2C' + property color bg1Hover: "#292929" + property color bg1Active: '#252525' property color bg1Border: '#bebebe' property color bg2Base: "#313131" - property color bg2: '#8a8a8a' - property color bg2Hover: '#b1b1b1' - property color bg2Active: '#919191' - property color bg2Border: '#c4c4c4' + property color bg2: '#313131' + property color bg2Hover: '#363636' + property color bg2Active: '#2B2B2B' + property color bg2Border: '#404040' property color subfg: "#CED1D7" property color fg: "#FFFFFF" property color fg1: "#D1D1D1" @@ -80,44 +81,59 @@ Singleton { property color controlBgHover: "#CFCED1" property color controlFg: "#454545" property color accentUnfocused: "#989898" + property color link: "#A7C9FC" + property color inputBg: ColorUtils.transparentize(darkColors.bg0, 0.5) } colors: QtObject { id: colors + // Special property color shadow: ColorUtils.transparentize('#161616', 0.62) property color ambientShadow: ColorUtils.transparentize("#000000", 0.75) - property color bgPanelFooterBase: ColorUtils.transparentize(root.dark ? root.darkColors.bgPanelFooter : root.lightColors.bgPanelFooter, root.panelBackgroundTransparency) - property color bgPanelFooter: ColorUtils.transparentize(root.dark ? root.darkColors.bgPanelFooter : root.lightColors.bgPanelFooter, root.panelLayerTransparency) - property color bgPanelBody: ColorUtils.transparentize(root.dark ? root.darkColors.bgPanelBody : root.lightColors.bgPanelBody, root.panelLayerTransparency) - property color bgPanelSeparator: ColorUtils.transparentize(root.dark ? root.darkColors.bgPanelSeparator : root.lightColors.bgPanelSeparator, root.backgroundTransparency) - property color bg0Opaque: root.dark ? root.darkColors.bg0 : root.lightColors.bg0 - property color bg0: ColorUtils.transparentize(bg0Opaque, root.backgroundTransparency) + property color bgPanelFooterBase: root.dark ? root.darkColors.bg0 : root.lightColors.bg0 + property color bgPanelFooterBackground: ColorUtils.transparentize(root.dark ? root.darkColors.bg0 : root.lightColors.bg0, root.panelBackgroundTransparency) + property color bgPanelFooter: ColorUtils.transparentize(bgPanelFooterBackground, root.panelLayerTransparency) + property color bgPanelBodyBase: root.dark ? root.darkColors.bgPanelBody : root.lightColors.bgPanelBody + property color bgPanelBody: ColorUtils.solveOverlayColor(bgPanelFooterBackground,bgPanelBodyBase, 1 - root.panelLayerTransparency) + property color bgPanelSeparator: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bgPanelSeparator : root.lightColors.bgPanelSeparator, 1 - root.panelBackgroundTransparency) + // Layer 0 + property color bg0Base: root.dark ? root.darkColors.bg0 : root.lightColors.bg0 + property color bg0: ColorUtils.transparentize(bg0Base, root.backgroundTransparency) property color bg0Border: ColorUtils.transparentize(root.dark ? root.darkColors.bg0Border : root.lightColors.bg0Border, root.backgroundTransparency) - property color bg1Base: ColorUtils.transparentize(root.dark ? root.darkColors.bg1Base : root.lightColors.bg1Base, root.backgroundTransparency) - property color bg1: ColorUtils.transparentize(root.dark ? root.darkColors.bg1 : root.lightColors.bg1, root.contentTransparency) - property color bg1Hover: ColorUtils.transparentize(root.dark ? root.darkColors.bg1Hover : root.lightColors.bg1Hover, root.contentTransparency) - property color bg1Active: ColorUtils.transparentize(root.dark ? root.darkColors.bg1Active : root.lightColors.bg1Active, root.contentTransparency) - property color bg1Border: ColorUtils.transparentize(root.dark ? root.darkColors.bg1Border : root.lightColors.bg1Border, root.contentTransparency) - property color bg2Base: ColorUtils.transparentize(root.dark ? root.darkColors.bg2Base : root.lightColors.bg2Base, root.backgroundTransparency) - property color bg2: ColorUtils.transparentize(root.dark ? root.darkColors.bg2 : root.lightColors.bg2, root.contentTransparency) - property color bg2Hover: ColorUtils.transparentize(root.dark ? root.darkColors.bg2Hover : root.lightColors.bg2Hover, root.contentTransparency) - property color bg2Active: ColorUtils.transparentize(root.dark ? root.darkColors.bg2Active : root.lightColors.bg2Active, root.contentTransparency) - property color bg2Border: ColorUtils.transparentize(root.dark ? root.darkColors.bg2Border : root.lightColors.bg2Border, root.contentTransparency) + // Layer 1 + property color bg1Base: root.dark ? root.darkColors.bg1 : root.lightColors.bg1 + property color bg1: ColorUtils.solveOverlayColor(bg0Base, bg1Base, 1 - root.contentTransparency) + property color bg1Hover: ColorUtils.solveOverlayColor(bg0Base, root.dark ? root.darkColors.bg1Hover : root.lightColors.bg1Hover, 1 - root.contentTransparency) + property color bg1Active: ColorUtils.solveOverlayColor(bg0Base, root.dark ? root.darkColors.bg1Active : root.lightColors.bg1Active, 1 - root.contentTransparency) + property color bg1Border: ColorUtils.solveOverlayColor(bg0Base, root.dark ? root.darkColors.bg1Border : root.lightColors.bg1Border, 1 - root.contentTransparency) + // Layer 2 + property color bg2Base: root.dark ? root.darkColors.bg2 : root.lightColors.bg2 + property color bg2: ColorUtils.solveOverlayColor(bgPanelBodyBase, bg2Base, 1 - root.contentTransparency) + property color bg2Hover: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bg2Hover : root.lightColors.bg2Hover, 1 - root.contentTransparency) + property color bg2Active: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bg2Active : root.lightColors.bg2Active, 1 - root.contentTransparency) + property color bg2Border: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bg2Border : root.lightColors.bg2Border, 1 - root.contentTransparency) + // Foreground / Text property color subfg: root.dark ? root.darkColors.subfg : root.lightColors.subfg property color fg: root.dark ? root.darkColors.fg : root.lightColors.fg property color fg1: root.dark ? root.darkColors.fg1 : root.lightColors.fg1 property color inactiveIcon: root.dark ? root.darkColors.inactiveIcon : root.lightColors.inactiveIcon + property color link: root.dark ? root.darkColors.link : root.lightColors.link + // Controls property color controlBgInactive: root.dark ? root.darkColors.controlBgInactive : root.lightColors.controlBgInactive property color controlBg: root.dark ? root.darkColors.controlBg : root.lightColors.controlBg property color controlBgHover: root.dark ? root.darkColors.controlBgHover : root.lightColors.controlBgHover property color controlFg: root.dark ? root.darkColors.controlFg : root.lightColors.controlFg + property color inputBg: root.dark ? root.darkColors.inputBg : root.lightColors.inputBg property color danger: "#C42B1C" property color dangerActive: "#B62D1F" property color warning: "#FF9900" + // Accent property color accent: Appearance.colors.colPrimary property color accentHover: Appearance.colors.colPrimaryHover property color accentActive: Appearance.colors.colPrimaryActive property color accentUnfocused: root.dark ? root.darkColors.accentUnfocused : root.lightColors.accentUnfocused property color accentFg: ColorUtils.isDark(accent) ? "#FFFFFF" : "#000000" + property color selection: Appearance.colors.colPrimaryContainer + property color selectionFg: Appearance.colors.colOnPrimaryContainer } radius: QtObject { @@ -138,12 +154,19 @@ Singleton { property int thin: Font.Normal property int regular: Font.Medium property int strong: Font.DemiBold - property int stronger: Font.Bold + property int stronger: (Font.DemiBold + 2*Font.Bold) / 3 + property int strongest: Font.Bold } property QtObject pixelSize: QtObject { property real normal: 11 property real large: 13 property real larger: 15 + property real xlarger: 17 + } + property QtObject variableAxes: QtObject { + property var ui: ({ + "wdth": 25 + }) } } @@ -162,7 +185,7 @@ Singleton { property Component color: Component { ColorAnimation { - duration: 120 + duration: 80 easing.type: Easing.BezierSpline easing.bezierCurve: transition.easing.bezierCurve.easeIn } @@ -231,5 +254,13 @@ Singleton { easing.bezierCurve: transition.easing.bezierCurve.easeIn } } + + property Component scroll: Component { + NumberAnimation { + duration: 250 + easing.type: Easing.BezierSpline + easing.bezierCurve: [0.0, 0.0, 0.25, 1.0, 1, 1] + } + } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/VerticalPageIndicator.qml b/dots/.config/quickshell/ii/modules/waffle/looks/VerticalPageIndicator.qml new file mode 100644 index 000000000..bb46bdc1c --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/VerticalPageIndicator.qml @@ -0,0 +1,73 @@ +pragma ComponentBehavior: Bound +import Qt.labs.synchronizer +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +Column { + id: root + + property bool showArrows: true + property int currentIndex: 0 + property int count: 1 + signal clicked(int index) + signal increasePage() + signal decreasePage() + + visible: count > 1 + spacing: 6 + + NavigationArrow { + visible: root.showArrows + down: false + } + + Repeater { + model: root.count + delegate: MouseArea { + id: pageIndicator + required property int index + hoverEnabled: true + onClicked: root.clicked(index); + anchors.horizontalCenter: parent.horizontalCenter + implicitWidth: 6 + implicitHeight: 6 + + Circle { + anchors.centerIn: parent + diameter: (index === root.currentIndex || pageIndicator.containsMouse) && !pageIndicator.pressed ? 6 : 4 + color: pageIndicator.containsMouse ? Looks.colors.controlBgHover : Looks.colors.controlBg + } + } + } + + NavigationArrow { + visible: root.showArrows + down: true + } + + component NavigationArrow: FluentIcon { + id: navArrow + required property bool down + anchors.horizontalCenter: parent.horizontalCenter + implicitHeight: 12 + implicitWidth: 12 - (2 * upArea.containsPress) + icon: down ? "caret-down" : "caret-up" + color: upArea.containsMouse ? Looks.colors.controlBgHover : Looks.colors.controlBg + filled: true + opacity: ((down && root.currentIndex < root.count - 1) || (!down && root.currentIndex > 0)) ? 1 : 0 + MouseArea { + id: upArea + anchors.fill: parent + hoverEnabled: true + onClicked: navArrow.down ? root.increasePage() : root.decreasePage(); + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/AppIcon.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WAppIcon.qml similarity index 94% rename from dots/.config/quickshell/ii/modules/waffle/bar/AppIcon.qml rename to dots/.config/quickshell/ii/modules/waffle/looks/WAppIcon.qml index 48ff26104..bd0f2fce4 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/AppIcon.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WAppIcon.qml @@ -2,7 +2,6 @@ import QtQuick import org.kde.kirigami as Kirigami import qs.services import qs.modules.common -import qs.modules.waffle.looks Kirigami.Icon { id: root @@ -18,4 +17,6 @@ Kirigami.Icon { roundToIconSize: false fallback: root.iconName source: tryCustomIcon ? `${Looks.iconsPath}/${root.iconName}${!root.separateLightDark ? "" : Looks.dark ? "-dark" : "-light"}.svg` : fallback + + color: Looks.colors.fg } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WBarAttachedPanelContent.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WBarAttachedPanelContent.qml index 3035a6511..97ee49d1e 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WBarAttachedPanelContent.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WBarAttachedPanelContent.qml @@ -1,3 +1,4 @@ +pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Qt5Compat.GraphicalEffects @@ -10,13 +11,13 @@ import qs.modules.waffle.looks Item { id: root - signal closed() + signal closed - property alias border: borderRect - property alias borderColor: borderRect.border.color required property Item contentItem property real visualMargin: 12 property int closeAnimDuration: 150 + property bool revealFromSides: false + property bool revealFromLeft: true function close() { closeAnim.start(); @@ -24,32 +25,28 @@ Item { readonly property bool barAtBottom: Config.options.waffles.bar.bottom - implicitHeight: borderRect.implicitHeight - implicitWidth: borderRect.implicitWidth + implicitHeight: contentItem.implicitHeight + visualMargin * 2 + implicitWidth: contentItem.implicitWidth + visualMargin * 2 - WRectangularShadow { - target: borderRect + focus: true + Keys.onPressed: event => { // Esc to close + if (event.key === Qt.Key_Escape) { + content.close(); + } } - Rectangle { - id: borderRect - z: 1 - - color: "transparent" - radius: Looks.radius.large - border.color: Looks.colors.bg2Border - border.width: 1 - implicitWidth: contentItem.implicitWidth + border.width * 2 - implicitHeight: contentItem.implicitHeight + border.width * 2 - + Item { + id: panelContent anchors { - left: parent.left - right: parent.right - top: root.barAtBottom ? undefined : parent.top - bottom: root.barAtBottom ? parent.bottom : undefined + left: (root.revealFromSides && !root.revealFromLeft) ? undefined : parent.left + right: (root.revealFromSides && root.revealFromLeft) ? undefined : parent.right + top: (!root.revealFromSides && root.barAtBottom) ? undefined : parent.top + bottom: (!root.revealFromSides && !root.barAtBottom) ? undefined : parent.bottom // Opening anim - bottomMargin: root.barAtBottom ? sourceEdgeMargin : 0 - topMargin: root.barAtBottom ? 0 : sourceEdgeMargin + bottomMargin: (!root.revealFromSides && root.barAtBottom) ? sourceEdgeMargin : root.visualMargin + topMargin: (!root.revealFromSides && !root.barAtBottom) ? sourceEdgeMargin : root.visualMargin + leftMargin: (root.revealFromSides && root.revealFromLeft) ? sideEdgeMargin : root.visualMargin + rightMargin: (root.revealFromSides && !root.revealFromLeft) ? sideEdgeMargin : root.visualMargin } Component.onCompleted: { @@ -57,24 +54,22 @@ Item { } property real sourceEdgeMargin: -(implicitHeight + root.visualMargin) - PropertyAnimation { + property real sideEdgeMargin: -(implicitWidth + root.visualMargin) + OpenAnim { id: openAnim - target: borderRect - property: "sourceEdgeMargin" - to: 0 - duration: 200 - easing.type: Easing.BezierSpline - easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + properties: "sourceEdgeMargin, sideEdgeMargin" } SequentialAnimation { id: closeAnim - PropertyAnimation { - target: borderRect - property: "sourceEdgeMargin" - to: -(implicitHeight + root.visualMargin) - duration: root.closeAnimDuration - easing.type: Easing.BezierSpline - easing.bezierCurve: Looks.transition.easing.bezierCurve.easeOut + ParallelAnimation { + CloseAnim { + property: "sourceEdgeMargin" + to: -(implicitHeight + root.visualMargin) + } + CloseAnim { + property: "sideEdgeMargin" + to: -(implicitWidth + root.visualMargin) + } } ScriptAction { script: { @@ -82,23 +77,22 @@ Item { } } } - } - - Item { - id: contentArea - z: 0 - anchors.fill: borderRect - anchors.margins: borderRect.border.width - implicitWidth: contentItem.implicitWidth - implicitHeight: contentItem.implicitHeight - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: contentArea.width - height: contentArea.height - radius: borderRect.radius - borderRect.border.width - } - } + implicitWidth: root.contentItem.implicitWidth + implicitHeight: root.contentItem.implicitHeight children: [root.contentItem] } + + component OpenAnim: PropertyAnimation { + target: panelContent + to: root.visualMargin + duration: 200 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + component CloseAnim: PropertyAnimation { + target: panelContent + duration: root.closeAnimDuration + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeOut + } } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WBorderedButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WBorderedButton.qml new file mode 100644 index 000000000..b3377cc64 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WBorderedButton.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +WButton { + id: root + + colBackground: Looks.colors.bg2 + colBackgroundHover: Looks.colors.bg2Hover + colBackgroundActive: Looks.colors.bg2Active + property color colBorder: Looks.colors.bg2Border + property color colBorderToggled: Looks.colors.accent + border.color: checked ? colBorderToggled : colBorder + border.width: root.pressed ? 2 : 1 +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WPanelFooterButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WBorderlessButton.qml similarity index 92% rename from dots/.config/quickshell/ii/modules/waffle/looks/WPanelFooterButton.qml rename to dots/.config/quickshell/ii/modules/waffle/looks/WBorderlessButton.qml index 0f0d99ad9..7f3546a6b 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WPanelFooterButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WBorderlessButton.qml @@ -25,8 +25,10 @@ Button { return root.colBackground } } + property alias radius: background.radius background: Rectangle { + id: background radius: Looks.radius.medium color: root.color } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WButton.qml index 69f255ad9..d2cef0634 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WButton.qml @@ -17,13 +17,14 @@ Button { property color colBackgroundToggledActive: Looks.colors.accentActive property color colForeground: Looks.colors.fg property color colForegroundToggled: Looks.colors.accentFg + property color colForegroundDisabled: ColorUtils.transparentize(Looks.colors.subfg, 0.4) property alias backgroundOpacity: backgroundRect.opacity property color color: { if (!root.enabled) return colBackground; if (root.checked) { if (root.down) { return root.colBackgroundToggledActive; - } else if (root.hovered && !root.down) { + } else if (root.hovered) { return root.colBackgroundToggledHover; } else { return root.colBackgroundToggled; @@ -31,13 +32,18 @@ Button { } if (root.down) { return root.colBackgroundActive; - } else if (root.hovered && !root.down) { + } else if (root.hovered) { return root.colBackgroundHover; } else { return root.colBackground; } } - property color fgColor: root.checked ? root.colForegroundToggled : root.colForeground + property color fgColor: { + if (!root.enabled) return root.colForegroundDisabled + if (root.checked) return root.colForegroundToggled + if (root.enabled) return root.colForeground + return root.colForeground + } property alias horizontalAlignment: buttonText.horizontalAlignment font { family: Looks.font.family.ui @@ -48,10 +54,11 @@ Button { // Hover stuff signal hoverTimedOut property bool shouldShowTooltip: false + ToolTip.delay: 400 property Timer hoverTimer: Timer { id: hoverTimer running: root.hovered - interval: 400 + interval: root.ToolTip.delay onTriggered: { root.hoverTimedOut(); } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WChoiceButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WChoiceButton.qml index eff776601..14691c716 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WChoiceButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WChoiceButton.qml @@ -44,6 +44,7 @@ WButton { radius: Looks.radius.medium color: root.color Behavior on color { + enabled: root.animateChoiceHighlight animation: Looks.transition.color.createObject(this) } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WIcons.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WIcons.qml index 1d24170bc..b804aa3d9 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WIcons.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WIcons.qml @@ -7,6 +7,10 @@ import qs.services Singleton { id: root + function pathForName(iconName) { + return Quickshell.shellPath(`assets/icons/fluent/${iconName}.svg`); + } + function wifiIconForStrength(strength) { if (strength > 75) return "wifi-1"; @@ -44,7 +48,7 @@ Singleton { } property string batteryLevelIcon: { - const discreteLevel = Math.ceil(Battery.percentage * 10) + const discreteLevel = Math.ceil(Battery.percentage * 10); return `battery-${discreteLevel > 9 ? "full" : discreteLevel}`; } @@ -103,7 +107,8 @@ Singleton { function audioAppIcon(node) { let icon; icon = AppSearch.guessIcon(node?.properties["application.icon-name"] ?? ""); - if (AppSearch.iconExists(icon)) return icon; + if (AppSearch.iconExists(icon)) + return icon; icon = AppSearch.guessIcon(node?.properties["node.name"] ?? ""); return icon; } @@ -123,4 +128,60 @@ Singleton { return "bluetooth"; } + function fluentFromMaterial(icon) { + switch (icon) { + case "calculate": + return "calculator"; + case "keyboard_return": + return "arrow-enter-left"; + case "open_in_new": + return "open"; + case "settings_suggest": + return "wand"; + case "terminal": + return "app-generic"; + case "travel_explore": + return "globe-search"; + case "keep": + return "pin"; + case "keep_off": + return "pin-off"; + default: + return "apps"; + } + } + + function guessIconForName(name) { + const lowerName = name.toLowerCase(); + if (lowerName.includes("app") || lowerName.includes("desktop")) + return "apps"; + if (lowerName.includes("news")) + return "news"; + if (lowerName.includes("new") || lowerName.includes("create") || lowerName.includes("add")) + return "add"; + if (lowerName.includes("open")) + return "open"; + if (lowerName.includes("friends") || lowerName.includes("contact") || lowerName.includes("family")) + return "people"; + if (lowerName.includes("community")) + return "people-team"; + if (lowerName.includes("library")) + return "library"; + if (lowerName.includes("setting")) + return "settings"; + if (lowerName.includes("gallery")) + return "image-copy"; + if (lowerName.includes("server")) + return "server"; + if (lowerName.includes("picture") || lowerName.includes("photo") || lowerName.includes("image")) + return "image"; + if (lowerName.includes("store") || lowerName.includes("shop")) + return "store-microsoft"; + if (lowerName.includes("record") || lowerName.includes("capture")) + return "record"; + if (lowerName.includes("screen") || lowerName.includes("display") || lowerName.includes("monitor") || lowerName.includes("desktop")) + return "desktop"; + + return "apps"; + } } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WListView.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WListView.qml new file mode 100644 index 000000000..3e506ef99 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WListView.qml @@ -0,0 +1,19 @@ +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Controls + +ListView { + id: root + + boundsBehavior: Flickable.DragOverBounds + + ScrollBar.vertical: WScrollBar {} + + displaced: Transition { + animations: [Looks.transition.enter.createObject(this, { + property: "y" + })] + } + +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WMenu.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WMenu.qml new file mode 100644 index 000000000..1d30576ec --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WMenu.qml @@ -0,0 +1,104 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +Menu { + id: root + + property bool downDirection: false + property bool hasIcons: false // TODO: implement + + property color color: Looks.colors.bg1Base + property alias backgroundPane: bgPane + + implicitWidth: background.implicitWidth + margins * 2 + implicitHeight: background.implicitHeight + margins * 2 + margins: 10 + padding: 3 + property real sourceEdgeMargin: -implicitHeight + clip: true + + enter: Transition { + NumberAnimation { + property: "sourceEdgeMargin" + from: -root.implicitHeight + to: root.margins + duration: 200 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + } + exit: Transition { + NumberAnimation { + property: "sourceEdgeMargin" + from: root.margins + to: -root.implicitHeight + duration: 150 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeOut + } + } + + background: Item { + id: bgItem + implicitWidth: bgPane.implicitWidth + implicitHeight: bgPane.implicitHeight + WPane { + id: bgPane + anchors { + left: parent.left + right: parent.right + top: root.downDirection ? parent.top : undefined + bottom: root.downDirection ? undefined : parent.bottom + margins: root.margins + topMargin: root.downDirection ? root.sourceEdgeMargin : root.margins + bottomMargin: root.downDirection ? root.margins : root.sourceEdgeMargin + } + contentItem: Rectangle { + color: root.color + implicitWidth: menuListView.implicitWidth + root.padding * 2 + implicitHeight: root.contentItem.implicitHeight + root.padding * 2 + } + + } + } + + Component.onCompleted: { + menuListView.itemAtIndex(0)?.forceActiveFocus(); + } + + contentItem: Item { + implicitWidth: menuListView.implicitWidth + implicitHeight: menuListView.implicitHeight + WListView { + id: menuListView + interactive: contentHeight > height + anchors { + left: parent.left + right: parent.right + top: root.downDirection ? parent.top : undefined + bottom: root.downDirection ? undefined : parent.bottom + margins: root.margins // ???? + topMargin: root.downDirection ? root.sourceEdgeMargin : root.margins + bottomMargin: root.downDirection ? root.margins : root.sourceEdgeMargin + } + clip: true + implicitHeight: contentHeight + implicitWidth: Array.from({ + length: count + }, (_, i) => itemAtIndex(i)?.implicitWidth ?? 0).reduce((a, b) => a > b ? a : b) + + model: root.contentModel + } + } + + delegate: WMenuItem { + id: menuItemDelegate + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WMenuItem.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WMenuItem.qml new file mode 100644 index 000000000..0a27c6cb3 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WMenuItem.qml @@ -0,0 +1,127 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +MenuItem { + id: root + + property color colBackground: ColorUtils.transparentize(Looks.colors.bg1) + property color colBackgroundHover: Looks.colors.bg2Hover + property color colBackgroundActive: Looks.colors.bg2Active + property color colBackgroundToggled: Looks.colors.bg2Hover + property color colBackgroundToggledHover: Looks.colors.bg2Active + property color colBackgroundToggledActive: Looks.colors.bg2Hover + property color colForeground: Looks.colors.fg + property color colForegroundToggled: Looks.colors.fg + property color colForegroundDisabled: ColorUtils.transparentize(Looks.colors.subfg, 0.4) + property color color: { + if (!root.enabled) + return colBackground; + if (root.checked) { + if (root.down) { + return root.colBackgroundToggledActive; + } else if (root.hovered) { + return root.colBackgroundToggledHover; + } else { + return root.colBackgroundToggled; + } + } + if (root.down) { + return root.colBackgroundActive; + } else if (root.hovered) { + return root.colBackgroundHover; + } else { + return root.colBackground; + } + } + property color fgColor: { + if (root.checked) + return root.colForegroundToggled; + if (root.enabled) + return root.colForeground; + return root.colForegroundDisabled; + } + + property real inset: 2 + topInset: inset + bottomInset: inset + leftInset: inset + rightInset: inset + horizontalPadding: 11 + + width: ListView.view?.width + height: visible ? implicitHeight : 0 + + background: Rectangle { + id: backgroundRect + radius: Looks.radius.medium + color: root.color + Behavior on color { + animation: Looks.transition.color.createObject(this) + } + } + + implicitHeight: Math.max(28, contentItem.implicitHeight) + topInset + bottomInset + implicitWidth: contentItem.implicitWidth + leftInset + rightInset + leftPadding + rightPadding + + contentItem: Item { + implicitWidth: contentLayout.implicitWidth + implicitHeight: contentLayout.implicitHeight + + RowLayout { + id: contentLayout + anchors.fill: parent + spacing: 12 + FluentIcon { + id: buttonIcon + monochrome: true + implicitSize: 20 + Layout.fillWidth: false + Layout.alignment: Qt.AlignVCenter + color: root.fgColor + visible: root.icon.name !== "" + icon: root.icon.name + } + WText { + id: buttonText + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + text: root.text + horizontalAlignment: Text.AlignLeft + font.pixelSize: Looks.font.pixelSize.large + color: root.fgColor + } + } + + WFadeLoader { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: -root.leftPadding + width + } + shown: root.checked + sourceComponent: Rectangle { + implicitWidth: 3 + implicitHeight: 3 + radius: width / 2 + color: Looks.colors.accent + property bool forceZeroHeight: true + height: forceZeroHeight ? 0 : Math.max(root.down ? 10 : 16, root.background.height - 18 * 2) + Component.onCompleted: { + forceZeroHeight = false; + } + + Behavior on height { + animation: Looks.transition.resize.createObject(this) + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WMouseAreaButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WMouseAreaButton.qml new file mode 100644 index 000000000..181ce6047 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WMouseAreaButton.qml @@ -0,0 +1,45 @@ +import QtQuick +import qs +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +MouseArea { + id: root + + property real radius: Looks.radius.medium + hoverEnabled: true + + property color colBackground: ColorUtils.transparentize(Looks.colors.bg2) + property color colBackgroundHover: Looks.colors.bg2Hover + property color colBackgroundActive: Looks.colors.bg2Active + property color colBorder: ColorUtils.transparentize(Looks.colors.bg2Border) + property color colBorderHover: Looks.colors.bg2Border + + property color color: { + if (containsMouse) { + return pressed ? colBackgroundActive : colBackgroundHover; + } else { + return colBackground; + } + } + + property color borderColor: { + if (containsMouse) { + return colBorderHover; + } else { + return colBorder; + } + } + + property Item background: Rectangle { + id: bgRect + parent: root + anchors.fill: parent + color: root.color + radius: root.radius + + border.color: root.borderColor + border.width: 1 + } +} \ No newline at end of file diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WPane.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WPane.qml new file mode 100644 index 000000000..01f20d8d2 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WPane.qml @@ -0,0 +1,60 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.waffle.looks + +Item { + id: root + property Item contentItem + property real radius: Looks.radius.large + property alias color: contentRect.color + property alias border: borderRect + property alias borderColor: borderRect.border.color + property alias borderWidth: borderRect.border.width + + implicitWidth: borderRect.implicitWidth + implicitHeight: borderRect.implicitHeight + + WRectangularShadow { + target: borderRect + } + + Rectangle { + id: borderRect + z: 1 + + color: "transparent" + radius: root.radius + border.color: Looks.colors.bg2Border + border.width: 1 + implicitWidth: contentItem.implicitWidth + border.width * 2 + implicitHeight: contentItem.implicitHeight + border.width * 2 + anchors.fill: contentRect + anchors.margins: -border.width + } + + Rectangle { + id: contentRect + anchors.centerIn: parent + z: 0 + + color: Looks.colors.bgPanelFooterBackground + implicitWidth: contentItem.implicitWidth + implicitHeight: contentItem.implicitHeight + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + id: contentAreaMask + width: contentRect.width + height: contentRect.height + radius: root.radius - borderRect.border.width + } + } + children: [root.contentItem] + } +} \ No newline at end of file diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WPanelIconButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WPanelIconButton.qml index e0e966022..3cdb3abfd 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WPanelIconButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WPanelIconButton.qml @@ -10,14 +10,17 @@ WButton { id: root property alias iconName: iconContent.icon + property alias iconSize: iconContent.implicitSize property alias monochrome: iconContent.monochrome implicitWidth: 40 implicitHeight: 40 - contentItem: FluentIcon { - id: iconContent - anchors.centerIn: parent - implicitSize: 18 - icon: root.iconName + contentItem: Item { + FluentIcon { + id: iconContent + anchors.centerIn: parent + implicitSize: 18 + icon: root.iconName + } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/PageColumn.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WPanelPageColumn.qml similarity index 100% rename from dots/.config/quickshell/ii/modules/waffle/actionCenter/PageColumn.qml rename to dots/.config/quickshell/ii/modules/waffle/looks/WPanelPageColumn.qml diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/Separator.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WPanelSeparator.qml similarity index 100% rename from dots/.config/quickshell/ii/modules/waffle/actionCenter/Separator.qml rename to dots/.config/quickshell/ii/modules/waffle/looks/WPanelSeparator.qml diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WRectangularShadowThis.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WRectangularShadowThis.qml new file mode 100644 index 000000000..5ad46de29 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WRectangularShadowThis.qml @@ -0,0 +1,15 @@ +import QtQuick +import QtQuick.Effects +import qs.modules.common +import qs.modules.common.widgets + +Item { + default property Item contentItem + property Item shadow: WRectangularShadow { + target: contentItem + } + implicitWidth: contentItem.implicitWidth + implicitHeight: contentItem.implicitHeight + + children: [shadow, contentItem] +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WScrollBar.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WScrollBar.qml new file mode 100644 index 000000000..bdc962806 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WScrollBar.qml @@ -0,0 +1,25 @@ +import QtQuick +import QtQuick.Controls +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions + +ScrollBar { + id: root + + policy: ScrollBar.AsNeeded + active: hovered || pressed + property color color: Looks.colors.controlBg + + contentItem: Rectangle { + implicitWidth: root.active ? 4 : 2 + implicitHeight: root.visualSize + radius: 9999 + color: root.color + + opacity: root.policy === ScrollBar.AlwaysOn || (root.active && root.size < 1.0) ? 0.5 : 0 + Behavior on opacity { + animation: Looks.transition.opacity.createObject(this) + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WStackView.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WStackView.qml index 218ec15fa..b566c91ab 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WStackView.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WStackView.qml @@ -11,11 +11,7 @@ StackView { property list fadeBezierCurve: Looks.transition.easing.bezierCurve.easeInOut clip: true - property alias color: background.color - background: Rectangle { - id: background - color: Looks.colors.bgPanelFooterBase - } + background: null pushEnter: Transition { XAnimator { diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WText.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WText.qml index 240fd63be..ab6dd60b7 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WText.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WText.qml @@ -8,8 +8,12 @@ Text { color: Looks.colors.fg font { + hintingPreference: Font.PreferDefaultHinting family: Looks.font.family.ui pixelSize: Looks.font.pixelSize.normal weight: Looks.font.weight.regular + variableAxes: Looks.font.variableAxes.ui } + + linkColor: Looks.colors.link } diff --git a/dots/.config/quickshell/ii/modules/waffle/actionCenter/FooterMoreButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WTextButton.qml similarity index 100% rename from dots/.config/quickshell/ii/modules/waffle/actionCenter/FooterMoreButton.qml rename to dots/.config/quickshell/ii/modules/waffle/looks/WTextButton.qml diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WTextField.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WTextField.qml new file mode 100644 index 000000000..a3cc9dbc0 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WTextField.qml @@ -0,0 +1,31 @@ +import qs.modules.common +import QtQuick +import QtQuick.Controls.FluentWinUI3 +import QtQuick.Controls + +TextField { + id: root + + clip: true + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + color: Looks.colors.fg + + palette { + active: Looks.colors.accent + } + + font { + hintingPreference: Font.PreferDefaultHinting + family: Looks.font.family.ui + pixelSize: Looks.font.pixelSize.normal + weight: Looks.font.weight.regular + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + cursorShape: Qt.IBeamCursor + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WTextInput.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WTextInput.qml new file mode 100644 index 000000000..84bed180d --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WTextInput.qml @@ -0,0 +1,19 @@ +import QtQuick +import QtQuick.Controls + +TextInput { + id: root + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + color: Looks.colors.fg + + font { + hintingPreference: Font.PreferFullHinting + family: Looks.font.family.ui + pixelSize: Looks.font.pixelSize.large + weight: Looks.font.weight.regular + } + + selectionColor: Looks.colors.selection + selectedTextColor: Looks.colors.selectionFg +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WToolTip.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WToolTip.qml index 3c8d20d26..7368836cc 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WToolTip.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WToolTip.qml @@ -25,6 +25,8 @@ StyledToolTip { verticalPadding: 8 horizontalPadding: 10 + delay: 400 + contentItem: WToolTipContent { id: tooltipContent realContentItem: root.realContentItem diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WToolTipContent.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WToolTipContent.qml index 13c78fc7e..b0a400df0 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WToolTipContent.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WToolTipContent.qml @@ -6,6 +6,7 @@ Item { id: root anchors.centerIn: parent required property Item realContentItem + property alias radius: realContent.radius property real verticalPadding: 8 property real horizontalPadding: 10 implicitWidth: realContent.implicitWidth + 2 * 2 diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WToolbar.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbar.qml new file mode 100644 index 000000000..6d58e8f7c --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbar.qml @@ -0,0 +1,38 @@ +import QtQuick +import QtQuick.Layouts +import qs.modules.common +import qs.modules.common.widgets + +Item { + id: root + + property real padding: 9 + property alias colBackground: background.color + property alias spacing: toolbarLayout.spacing + property alias radius: background.radius + default property alias data: toolbarLayout.data + + implicitWidth: background.implicitWidth + implicitHeight: background.implicitHeight + + Rectangle { + id: background + anchors.fill: parent + implicitHeight: 50 + implicitWidth: toolbarLayout.implicitWidth + root.padding * 2 + radius: Looks.radius.large + color: Looks.colors.bg0Base + + border.width: 1 + border.color: Looks.colors.bg1Border + + RowLayout { + id: toolbarLayout + spacing: 4 + anchors { + fill: parent + margins: root.padding + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarButton.qml new file mode 100644 index 000000000..59dc5ee99 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarButton.qml @@ -0,0 +1,8 @@ +import QtQuick +import QtQuick.Layouts +import qs.modules.common + +WButton { + implicitHeight: 32 + radius: Looks.radius.medium +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarIconButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarIconButton.qml new file mode 100644 index 000000000..4a3644735 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarIconButton.qml @@ -0,0 +1,16 @@ +import QtQuick +import QtQuick.Layouts +import qs.modules.common + +WToolbarButton { + id: root + implicitWidth: height + contentItem: Item { + FluentIcon { + anchors.centerIn: parent + icon: root.icon.name + implicitSize: 18 + color: root.fgColor + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarIconTabButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarIconTabButton.qml new file mode 100644 index 000000000..0dc6ae208 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarIconTabButton.qml @@ -0,0 +1,21 @@ +import QtQuick +import QtQuick.Controls +import qs.modules.common + +TabButton { + id: root + + implicitWidth: 38 + implicitHeight: 32 + padding: 0 + + background: null + contentItem: Item { + FluentIcon { + anchors.centerIn: parent + icon: root.icon.name + color: root.icon.color + implicitSize: 18 + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarSeparator.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarSeparator.qml new file mode 100644 index 000000000..d67c4b767 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarSeparator.qml @@ -0,0 +1,11 @@ +import QtQuick +import QtQuick.Layouts +import qs.modules.common + +Rectangle { + Layout.leftMargin: 4 + Layout.rightMargin: 4 + implicitHeight: 24 + implicitWidth: 1 + color: Looks.colors.bg0Border +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarTabBar.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarTabBar.qml new file mode 100644 index 000000000..025ee53a4 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WToolbarTabBar.qml @@ -0,0 +1,53 @@ +import QtQuick +import QtQuick.Controls +import qs.modules.common +import qs.modules.common.functions + +TabBar { + id: root + implicitHeight: 32 + + background: Rectangle { + radius: Looks.radius.medium + color: Looks.colors.bgPanelFooter + border.color: ColorUtils.transparentize(Looks.colors.bg0Border, 0.7) + border.width: 1 + + // Indicator + Rectangle { + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + leftMargin: root.currentIndex * (root.width / root.count) + Behavior on leftMargin { + animation: Looks.transition.resize.createObject(this) + } + } + radius: Looks.radius.medium + color: Looks.colors.bg2Base + border.color: Looks.colors.bg0Border + border.width: 1 + width: root.width / root.count + + Rectangle { + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: 1 + } + implicitWidth: pressDetector.containsPress ? 16 : 12 + implicitHeight: 3 + radius: height / 2 + color: Looks.colors.accent + } + } + } + + MouseArea { + id: pressDetector + z: 9999 + anchors.fill: parent + acceptedButtons: Qt.LeftButton + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WUserAvatar.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WUserAvatar.qml new file mode 100644 index 000000000..07499d51b --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WUserAvatar.qml @@ -0,0 +1,27 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +StyledImage { + id: avatar + Layout.alignment: Qt.AlignTop + sourceSize: Qt.size(32, 32) + source: Directories.userAvatarPathAccountsService + fallbacks: [Directories.userAvatarPathRicersAndWeirdSystems, Directories.userAvatarPathRicersAndWeirdSystems2] + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Circle { + diameter: avatar.height + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/CalendarWidget.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/CalendarWidget.qml new file mode 100644 index 000000000..7c01a0b01 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/CalendarWidget.qml @@ -0,0 +1,142 @@ +pragma ComponentBehavior: Bound +import QtQml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +BodyRectangle { + id: root + + // State + property bool collapsed + + // Locale + property var locale: Qt.locale(Config.options.calendar.locale) + + implicitHeight: collapsed ? 0 : contentColumn.implicitHeight + implicitWidth: contentColumn.implicitWidth + + Behavior on implicitHeight { + animation: Looks.transition.enter.createObject(this) + } + + clip: true + ColumnLayout { + id: contentColumn + spacing: 12 + CalendarHeader { + Layout.topMargin: 10 + Layout.fillWidth: true + } + ColumnLayout { + Layout.fillWidth: true + Layout.leftMargin: 5 + Layout.rightMargin: 5 + spacing: 1 + DayOfWeekRow { + Layout.fillWidth: true + locale: root.locale + spacing: calendarView.buttonSpacing + implicitHeight: calendarView.buttonSize + delegate: Item { + id: dayOfWeekItem + required property var model + implicitHeight: calendarView.buttonSize + implicitWidth: calendarView.buttonSize + WText { + anchors.centerIn: parent + text: { + var result = dayOfWeekItem.model.shortName; + if (Config.options.waffles.calendar.force2CharDayOfWeek) result = result.substring(0,2); + return result; + } + color: Looks.colors.fg + font.pixelSize: Looks.font.pixelSize.large + } + } + } + CalendarView { + id: calendarView + locale: root.locale + verticalPadding: 2 + buttonSize: 41 // ??? + buttonSpacing: 6 + buttonVerticalSpacing: 1 + Layout.fillWidth: true + delegate: DayButton {} + } + } + } + + component DayButton: WButton { + id: dayButton + required property var model + checked: model.today + enabled: hovered || calendarView.scrolling || checked || model.month === calendarView.focusedMonth + implicitWidth: calendarView.buttonSize + implicitHeight: calendarView.buttonSize + radius: height / 2 + + required property int index + + contentItem: Item { + WText { + anchors.centerIn: parent + text: dayButton.model.day + color: dayButton.fgColor + font.pixelSize: Looks.font.pixelSize.larger + } + } + } + + component CalendarHeader: RowLayout { + Layout.leftMargin: 8 + Layout.rightMargin: 8 + spacing: 8 + + WBorderlessButton { + Layout.fillWidth: true + implicitHeight: 34 + contentItem: Item { + WText { + anchors.fill: parent + horizontalAlignment: Text.AlignLeft + text: Qt.locale().toString(calendarView.focusedDate, "MMMM yyyy") + font.pixelSize: Looks.font.pixelSize.large + font.weight: Looks.font.weight.strong + } + } + } + ScrollMonthButton { + scrollDown: false + } + ScrollMonthButton { + scrollDown: true + } + } + + component ScrollMonthButton: WBorderlessButton { + id: scrollMonthButton + required property bool scrollDown + Layout.alignment: Qt.AlignVCenter + + onClicked: { + calendarView.scrollMonthsAndSnap(scrollDown ? 1 : -1); + } + implicitWidth: 32 + implicitHeight: 34 + + contentItem: FluentIcon { + filled: true + implicitSize: 12 + icon: scrollMonthButton.scrollDown ? "caret-down" : "caret-up" + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/DateHeader.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/DateHeader.qml new file mode 100644 index 000000000..99e0666ed --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/DateHeader.qml @@ -0,0 +1,55 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +FooterRectangle { + id: root + + implicitWidth: 0 + property bool collapsed + color: ColorUtils.transparentize(Looks.colors.bgPanelBody, collapsed ? 0 : 1) + Behavior on color { + animation: Looks.transition.color.createObject(this) + } + + RowLayout { + anchors { + fill: parent + leftMargin: 16 + rightMargin: 16 + topMargin: 12 + bottomMargin: 12 + } + + WText { + Layout.fillWidth: true + font.pixelSize: Looks.font.pixelSize.large + text: DateTime.collapsedCalendarFormat + } + + WBorderedButton { + implicitWidth: 24 + implicitHeight: 24 + padding: 0 + onClicked: root.collapsed = !root.collapsed + contentItem: Item { + FluentIcon { + anchors.centerIn: parent + implicitSize: 12 + icon: "chevron-down" + rotation: root.collapsed ? 180 : 0 + Behavior on rotation { + animation: Looks.transition.rotate.createObject(this) + } + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/FocusFooter.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/FocusFooter.qml new file mode 100644 index 000000000..2f2b788a4 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/FocusFooter.qml @@ -0,0 +1,72 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +FooterRectangle { + Layout.fillWidth: true + implicitWidth: 0 + color: Looks.colors.bgPanelBody + + RowLayout { + anchors { + fill: parent + leftMargin: 16 + rightMargin: 16 + topMargin: 12 + bottomMargin: 12 + } + spacing: 0 + + SmallBorderedIconButton { + visible: !TimerService.pomodoroRunning + icon.name: "subtract" + onClicked: Config.options.time.pomodoro.focus -= 300 // 5 mins + } + + WTextWithFixedWidth { + visible: !TimerService.pomodoroRunning + implicitWidth: 81 + horizontalAlignment: Text.AlignHCenter + color: Looks.colors.subfg + text: Translation.tr("%1 mins").arg(`${TimerService.focusTime / 60}`) + } + + SmallBorderedIconButton { + visible: !TimerService.pomodoroRunning + icon.name: "add" + onClicked: Config.options.time.pomodoro.focus += 300 // 5 mins + } + + WText { + visible: TimerService.pomodoroRunning + font.pixelSize: Looks.font.pixelSize.large + text: Translation.tr("Focusing") + } + + Item { + Layout.fillWidth: true + } + + SmallBorderedIconAndTextButton { + iconName: TimerService.pomodoroRunning ? "stop" : "play" + text: TimerService.pomodoroRunning ? Translation.tr("End session") : Translation.tr("Focus") + + onClicked: { + if (TimerService.pomodoroRunning) { + TimerService.togglePomodoro(); + TimerService.resetPomodoro(); + } else { + TimerService.togglePomodoro(); + Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "sidebarRight", "toggle"]); + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml new file mode 100644 index 000000000..f49477988 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml @@ -0,0 +1,99 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Qt.labs.synchronizer +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +WBarAttachedPanelContent { + id: root + + readonly property bool barAtBottom: Config.options.waffles.bar.bottom + revealFromSides: true + revealFromLeft: false + + property bool collapsed: false + + contentItem: ColumnLayout { + id: contentLayout + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + bottom: parent.bottom + } + spacing: 12 + + Item { + id: notificationArea + Layout.fillHeight: true + implicitWidth: notificationPane.implicitWidth + + WPane { + id: notificationPane + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + contentItem: NotificationPaneContent { + implicitWidth: calendarColumnLayout.implicitWidth + implicitHeight: { + if (Notifications.list.length > 0) { + return ((contentLayout.height - calendarPane.height - contentLayout.spacing) - notificationPane.borderWidth * 2) + } + return 230; + } + + Timer { + id: enableTimer + interval: Config.options.hacks.arbitraryRaceConditionDelay + onTriggered: heightBehavior.enabled = true; + } + Behavior on implicitHeight { + id: heightBehavior + enabled: false + Component.onCompleted: { + enableTimer.restart(); + } + animation: Looks.transition.enter.createObject(this) + } + } + } + } + + WPane { + id: calendarPane + contentItem: WPanelPageColumn { + id: calendarColumnLayout + DateHeader { + Layout.fillWidth: true + Synchronizer on collapsed { + property alias source: root.collapsed + } + } + + WPanelSeparator { + visible: !root.collapsed + } + + CalendarWidget { + Layout.fillWidth: true + Synchronizer on collapsed { + property alias source: root.collapsed + } + } + + WPanelSeparator {} + + FocusFooter { + Layout.fillWidth: true + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationHeaderButton.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationHeaderButton.qml new file mode 100644 index 000000000..9aa20a690 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationHeaderButton.qml @@ -0,0 +1,31 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +WBorderlessButton { + id: root + Layout.fillWidth: false + property real implicitSize: 16 + implicitWidth: implicitSize + implicitHeight: implicitSize + color: "transparent" + colForeground: root.hovered && !root.pressed ? Looks.colors.fg : Looks.colors.fg1 + + Behavior on colForeground { + animation: Looks.transition.color.createObject(this) + } + + contentItem: Item { + FluentIcon { + anchors.centerIn: parent + implicitSize: root.implicitSize + icon: root.icon.name + color: root.colForeground + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml new file mode 100644 index 000000000..96669e353 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml @@ -0,0 +1,80 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +FooterRectangle { + id: root + anchors.fill: parent + implicitHeight: 230 + + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: 4 + + spacing: 12 + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 12 + Layout.topMargin: 8 + + spacing: 8 + + WText { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + text: Translation.tr("Notifications") + font.pixelSize: Looks.font.pixelSize.large + } + + SmallBorderedIconButton { + icon.name: "alert-snooze" + checked: Notifications.silent + onClicked: { + Notifications.silent = !Notifications.silent; + } + } + + SmallBorderedIconAndTextButton { + visible: Notifications.list.length > 0 + iconVisible: false + text: Translation.tr("Clear all") + onClicked: { + Notifications.discardAllNotifications(); + } + } + } + + WListView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + model: Notifications.appNameList + delegate: WNotificationGroup { + required property int index + required property var modelData + width: ListView.view.width + notificationGroup: Notifications.groupsByAppName[modelData] + } + + EmptyPlaceholder { + visible: Notifications.list.length === 0 + anchors.centerIn: parent + } + } + } + + component EmptyPlaceholder: WText { + horizontalAlignment: Text.AlignHCenter + text: Translation.tr("No new notifications") + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconAndTextButton.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconAndTextButton.qml new file mode 100644 index 000000000..faab4d90d --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconAndTextButton.qml @@ -0,0 +1,43 @@ +import QtQuick +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +AcrylicButton { + id: root + + property bool iconVisible: true + property string iconName: "" + property bool iconFilled: true + + colBackground: Looks.colors.bg2 + colBackgroundHover: Looks.colors.bg2Hover + colBackgroundActive: Looks.colors.bg2Active + property color colBorder: Looks.colors.bg2Border + property color colBorderToggled: Looks.colors.accent + border.color: checked ? colBorderToggled : colBorder + + leftPadding: 12 + rightPadding: 12 + implicitWidth: focusButtonContent.implicitWidth + leftPadding + rightPadding + implicitHeight: 24 + + contentItem: Row { + id: focusButtonContent + spacing: 4 + + FluentIcon { + visible: root.iconVisible + icon: root.iconName + filled: root.iconFilled + implicitSize: 14 + anchors.verticalCenter: parent.verticalCenter + } + WText { + anchors.verticalCenter: parent.verticalCenter + text: root.text + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconButton.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconButton.qml new file mode 100644 index 000000000..f7006ca7f --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/SmallBorderedIconButton.qml @@ -0,0 +1,19 @@ +import QtQuick +import qs +import qs.services +import qs.modules.common +import qs.modules.waffle.looks + +WBorderedButton { + id: root + implicitWidth: 24 + implicitHeight: 24 + contentItem: Item { + FluentIcon { + anchors.centerIn: parent + implicitSize: 12 + icon: root.icon.name + color: root.fgColor + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationAppIcon.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationAppIcon.qml new file mode 100644 index 000000000..a658695d1 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationAppIcon.qml @@ -0,0 +1,30 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import org.kde.kirigami as Kirigami +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +Item { + id: root + + property string icon: "" + property real implicitSize: 16 + implicitWidth: implicitSize + implicitHeight: implicitSize + + Kirigami.Icon { + anchors.fill: parent + implicitWidth: root.implicitSize + implicitHeight: root.implicitSize + + source: root.icon || fallback + fallback: `${Looks.iconsPath}/apps.svg` + roundToIconSize: false + isMask: !root.icon + color: Looks.colors.fg + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationDismissAnim.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationDismissAnim.qml new file mode 100644 index 000000000..8d3972c40 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationDismissAnim.qml @@ -0,0 +1,32 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +SequentialAnimation { + id: root + + required property var target + + PropertyAction { + target: root.target + property: "ListView.delayRemove" + value: true + } + NumberAnimation { + target: root.target + property: "x" + to: root.target.width + duration: 250 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + PropertyAction { + target: root.target + property: "ListView.delayRemove" + value: false + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml new file mode 100644 index 000000000..88c44136e --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml @@ -0,0 +1,145 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +// TODO: Swipe to dismiss +MouseArea { + id: root + + required property var notificationGroup + readonly property var notifications: notificationGroup?.notifications ?? [] + property bool expanded: false + + implicitWidth: contentLayout.implicitWidth + implicitHeight: contentLayout.implicitHeight + + function dismissAll() { + root.notifications.forEach(notif => { + Qt.callLater(() => { + Notifications.discardNotification(notif.notificationId); + }); + }); + removeAnimation.start(); + } + + WNotificationDismissAnim { + id: removeAnimation + target: root + } + + property real dragDismissThreshold: 100 + drag { + axis: Drag.XAxis + target: contentLayout + minimumX: 0 + onActiveChanged: { + if (drag.active) + return; + if (contentLayout.x > root.dragDismissThreshold) { + root.dismissAll(); + } else { + contentLayout.x = 0; + } + } + } + + ColumnLayout { + id: contentLayout + spacing: 4 + width: root.width + + Behavior on x { + animation: Looks.transition.enter.createObject(this) + } + + GroupHeader { + id: notifHeader + Layout.fillWidth: true + Layout.margins: 11 + } + + WListView { + Layout.leftMargin: -Math.min(35, contentLayout.x) + Layout.rightMargin: -Layout.leftMargin + Layout.fillWidth: true + implicitWidth: notifHeader.implicitWidth + implicitHeight: contentHeight + interactive: false + spacing: 4 + model: ScriptModel { + values: root.expanded ? root.notifications.slice().reverse() : root.notifications.slice(-1) + objectProp: "notificationId" + } + delegate: WSingleNotification { + id: singleNotif + required property int index + required property var modelData + + width: ListView.view.width + notification: modelData + + groupExpandControlMessage: { + if (root.notifications.length <= 1) + return ""; + if (!root.expanded) + return Translation.tr("+%1 notifications").arg(root.notifications.length - 1); + if (index === root.notifications.length - 1) + return Translation.tr("See fewer"); + return ""; + } + onGroupExpandToggle: { + root.expanded = !root.expanded; + } + } + } + } + + component GroupHeader: MouseArea { + id: headerMouseArea + hoverEnabled: true + acceptedButtons: Qt.NoButton + + implicitWidth: appHeader.implicitWidth + implicitHeight: appHeader.implicitHeight + + RowLayout { + id: appHeader + anchors.fill: parent + spacing: 7 + + WNotificationAppIcon { + Layout.alignment: Qt.AlignVCenter + icon: root.notificationGroup?.appIcon ?? "" + } + + WText { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + text: root.notificationGroup?.appName ?? "" + } + + // NotificationHeaderButton { // TODO: More notification functionality needed so we can have this button + // visible: headerMouseArea.containsMouse + // Layout.leftMargin: 25 + // Layout.rightMargin: 25 + // icon.name: "more-horizontal" + // } + + NotificationHeaderButton { + visible: headerMouseArea.containsMouse + Layout.rightMargin: 3 + icon.name: "dismiss" + onClicked: { + root.dismissAll(); + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml new file mode 100644 index 000000000..e7fb58a12 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml @@ -0,0 +1,269 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Notifications +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +MouseArea { + id: root + + required property var notification + property bool expanded: notification.actions.length > 0 + property string groupExpandControlMessage: "" + + readonly property bool isPopup: notification?.popup ?? false + + signal groupExpandToggle + hoverEnabled: true + + function dismiss() { + Qt.callLater(() => { + Notifications.discardNotification(root.notification?.notificationId); + }); + removeAnimation.start(); + } + + WNotificationDismissAnim { + id: removeAnimation + target: root + } + + implicitHeight: contentItem.implicitHeight + implicitWidth: contentItem.implicitWidth + + Behavior on implicitHeight { + animation: Looks.transition.enter.createObject(this) + } + + property real dragDismissThreshold: 100 + drag { + axis: Drag.XAxis + target: contentItem + minimumX: 0 + onActiveChanged: { + if (drag.active) + return; + if (contentItem.x > root.dragDismissThreshold) { + root.dismiss(); + } else { + contentItem.x = 0; + } + } + } + + Rectangle { + id: contentItem + width: parent.width + color: root.isPopup ? Looks.colors.bg0 : Looks.colors.bgPanelBody + radius: root.isPopup ? Looks.radius.large : Looks.radius.medium + property real padding: 12 + implicitHeight: notificationContent.implicitHeight + padding * 2 + implicitWidth: notificationContent.implicitWidth + padding * 2 + border.width: 1 + border.color: root.isPopup ? Looks.colors.bg2Border : Looks.colors.bgPanelSeparator + + Behavior on x { + animation: Looks.transition.enter.createObject(this) + } + + ColumnLayout { + id: notificationContent + anchors.fill: parent + anchors.margins: contentItem.padding + spacing: 19 + + // Header + SingleNotificationHeader { + Layout.fillWidth: true + } + + // Content + Item { + id: actualContent + Layout.fillWidth: true + Layout.fillHeight: true + property real spacing: 16 + implicitHeight: Math.max(contentColumn.implicitHeight, imageLoader.height) + implicitWidth: contentColumn.implicitWidth + + Loader { + id: imageLoader + anchors { + top: parent.top + left: parent.left + } + active: root.notification.image != "" + sourceComponent: StyledImage { + readonly property int size: 48 + width: size + height: size + sourceSize.width: size + sourceSize.height: size + source: root.notification.image + fillMode: Image.PreserveAspectFit + } + } + + ColumnLayout { + id: contentColumn + anchors { + top: parent.top + left: parent.left + right: parent.right + } + spacing: 3 + + SummaryText { + id: summaryText + Layout.leftMargin: imageLoader.active ? imageLoader.width + actualContent.spacing : 0 + } + BodyText { + Layout.leftMargin: imageLoader.active ? imageLoader.width + actualContent.spacing : 0 + // onLineLaidOut: (line) => { + // if (!imageLoader.active) return; + // const dodgeDistance = imageLoader.width + actualContent.spacing; + // // print(line.y, dodgeDistance) + // if (summaryText.height + line.y > dodgeDistance) { + // line.x -= dodgeDistance; + // line.width += dodgeDistance; + // } + // } + } + } + } + + // Actions + ActionsRow { + Layout.fillWidth: true + } + + // "+1 notifications" button + GroupExpandButton { + Layout.bottomMargin: 2 + } + } + } + + component SingleNotificationHeader: RowLayout { + ExpandButton { + Layout.topMargin: -2 + } + + Item { + Layout.fillWidth: true + } + + NotificationHeaderButton { + Layout.rightMargin: 4 + opacity: (root.containsMouse || root.isPopup) ? 1 : 0 + icon.name: "dismiss" + implicitSize: 14 + onClicked: root.dismiss() + } + } + + component ActionsRow: RowLayout { + visible: root.expanded && root.notification.actions.length > 0 + uniformCellSizes: true + Repeater { + id: actionRepeater + model: root.notification.actions + delegate: WBorderedButton { + id: actionButton + Layout.fillHeight: true + required property var modelData + Layout.fillWidth: true + verticalPadding: 16 + horizontalPadding: 12 + text: modelData.text + implicitHeight: actionButtonText.implicitHeight + verticalPadding * 2 + contentItem: WText { + id: actionButtonText + text: actionButton.text + font.pixelSize: Looks.font.pixelSize.large + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + } + } + } + } + + component SummaryText: WText { + Layout.fillWidth: true + elide: Text.ElideRight + text: root.notification?.summary + font.pixelSize: Looks.font.pixelSize.large + } + + component BodyText: WText { + Layout.fillWidth: true + Layout.fillHeight: true + elide: Text.ElideRight + verticalAlignment: Text.AlignTop + wrapMode: Text.Wrap + maximumLineCount: root.expanded ? 100 : 1 + text: { + if (root.expanded) + return `` + `${NotificationUtils.processNotificationBody(root.notification.body, root.notification.appName || root.notification.summary).replace(/\n/g, "
")}`; + return NotificationUtils.processNotificationBody(root.notification.body, root.notification.appName || root.notification.summary).replace(/\n/g, "
"); + } + color: Looks.colors.subfg + textFormat: root.expanded ? Text.RichText : Text.StyledText + onLinkActivated: link => { + Qt.openUrlExternally(link); + GlobalStates.sidebarRightOpen = false; + } + } + + component ExpandButton: NotificationHeaderButton { + id: expandButton + implicitWidth: expandButtonContent.implicitWidth + onClicked: root.expanded = !root.expanded + + contentItem: Item { + id: expandButtonContent + implicitWidth: expandButtonRow.implicitWidth + implicitHeight: expandButtonRow.implicitHeight + RowLayout { + id: expandButtonRow + anchors.centerIn: parent + spacing: 8 + WText { + color: expandButton.colForeground + text: NotificationUtils.getFriendlyNotifTimeString(root.notification?.time) + } + FluentIcon { + Layout.rightMargin: 12 + icon: "chevron-down" + implicitSize: 18 + rotation: root.expanded ? -180 : 0 + color: expandButton.colForeground + Behavior on rotation { + animation: Looks.transition.rotate.createObject(this) + } + } + } + } + } + + component GroupExpandButton: AcrylicButton { + id: groupExpandButton + visible: root.groupExpandControlMessage !== "" + horizontalPadding: 10 + implicitHeight: 24 + implicitWidth: expandButtonText.implicitWidth + horizontalPadding * 2 + onClicked: root.groupExpandToggle() + contentItem: Item { + WText { + id: expandButtonText + anchors.centerIn: parent + text: root.groupExpandControlMessage + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WaffleNotificationCenter.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WaffleNotificationCenter.qml new file mode 100644 index 000000000..4caed7664 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WaffleNotificationCenter.qml @@ -0,0 +1,85 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets + +Scope { + id: root + + Connections { + target: GlobalStates + + function onSidebarRightOpenChanged() { + if (GlobalStates.sidebarRightOpen) panelLoader.active = true; + } + } + + Loader { + id: panelLoader + active: GlobalStates.sidebarRightOpen + sourceComponent: PanelWindow { + id: panelWindow + exclusiveZone: 0 + WlrLayershell.namespace: "quickshell:wNotificationCenter" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + color: "transparent" + + anchors { + bottom: true + top: true + right: true + } + + implicitWidth: content.implicitWidth + implicitHeight: content.implicitHeight + + HyprlandFocusGrab { + id: focusGrab + active: true + windows: [panelWindow] + onCleared: content.close(); + } + + Connections { + target: GlobalStates + function onSidebarRightOpenChanged() { + if (!GlobalStates.sidebarRightOpen) content.close(); + } + } + + NotificationCenterContent { + id: content + anchors.fill: parent + + onClosed: { + GlobalStates.sidebarRightOpen = false; + panelLoader.active = false; + } + } + } + } + + function toggleOpen() { + GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen; + } + + IpcHandler { + target: "sidebarRight" + + function toggle() { + root.toggleOpen(); + } + } + + GlobalShortcut { + name: "sidebarRightToggle" + description: "Toggles notification center on press" + + onPressed: root.toggleOpen(); + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationPopup/WaffleNotificationPopup.qml b/dots/.config/quickshell/ii/modules/waffle/notificationPopup/WaffleNotificationPopup.qml new file mode 100644 index 000000000..7deb54c30 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationPopup/WaffleNotificationPopup.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.waffle.looks +import qs.modules.waffle.notificationCenter + +Scope { + id: notificationPopup + + PanelWindow { + id: root + visible: (Notifications.popupList.length > 0) && !GlobalStates.screenLocked + screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? null + + WlrLayershell.namespace: "quickshell:notificationPopup" + WlrLayershell.layer: WlrLayer.Overlay + exclusiveZone: 0 + + anchors { + top: true + right: true + bottom: true + } + + mask: Region { + item: listview.contentItem + } + + color: "transparent" + implicitWidth: listview.implicitWidth + + WListView { + id: listview + anchors { + bottom: parent.bottom + right: parent.right + left: parent.left + } + leftMargin: 16 + rightMargin: 16 + topMargin: 16 + bottomMargin: 16 + + height: Math.min(contentItem.height + topMargin + bottomMargin, parent.height) + width: parent.width - Appearance.sizes.elevationMargin * 2 + + implicitWidth: 396 + spacing:12 + + model: ScriptModel { + values: Notifications.popupList + } + delegate: WSingleNotification { + required property var modelData + notification: modelData + width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/onScreenDisplay/OSDValue.qml b/dots/.config/quickshell/ii/modules/waffle/onScreenDisplay/OSDValue.qml index 8d48a6f92..32db99479 100644 --- a/dots/.config/quickshell/ii/modules/waffle/onScreenDisplay/OSDValue.qml +++ b/dots/.config/quickshell/ii/modules/waffle/onScreenDisplay/OSDValue.qml @@ -13,7 +13,6 @@ WBarAttachedPanelContent { required property string iconName property real value property bool showNumber: true - borderColor: Looks.colors.ambientShadow property Timer timer: Timer { id: autoCloseTimer @@ -21,44 +20,48 @@ WBarAttachedPanelContent { interval: Config.options.osd.timeout repeat: false onTriggered: { - root.close() + root.close(); } } - contentItem: Rectangle { + contentItem: WPane { anchors.centerIn: parent - color: Looks.colors.bg1Base - radius: Looks.radius.medium - implicitWidth: root.showNumber ? 192 : 170 - implicitHeight: 46 - - RowLayout { - id: contentRow - anchors.fill: parent - anchors.margins: 12 + borderColor: Looks.colors.ambientShadow - spacing: 12 + contentItem: Item { + // color: Looks.colors.bg1Base + // radius: Looks.radius.medium + implicitWidth: root.showNumber ? 192 : 170 + implicitHeight: 46 - FluentIcon { - Layout.alignment: Qt.AlignVCenter - icon: root.iconName - implicitSize: 18 - } + RowLayout { + id: contentRow + anchors.fill: parent + anchors.margins: 12 - WProgressBar { - id: progressBar - value: root.value - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - Layout.rightMargin: root.showNumber ? 0 : 3 - } + spacing: 12 - WTextWithFixedWidth { - visible: root.showNumber - text: Math.round(root.value * 100) - // longestText: "100" - implicitWidth: 16 - horizontalAlignment: Text.AlignHCenter + FluentIcon { + Layout.alignment: Qt.AlignVCenter + icon: root.iconName + implicitSize: 18 + } + + WProgressBar { + id: progressBar + value: root.value + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: root.showNumber ? 0 : 3 + } + + WTextWithFixedWidth { + visible: root.showNumber + text: Math.round(root.value * 100) + // longestText: "100" + implicitWidth: 16 + horizontalAlignment: Text.AlignHCenter + } } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/onScreenDisplay/WaffleOSD.qml b/dots/.config/quickshell/ii/modules/waffle/onScreenDisplay/WaffleOSD.qml index f2f06b2ae..569ed4f44 100644 --- a/dots/.config/quickshell/ii/modules/waffle/onScreenDisplay/WaffleOSD.qml +++ b/dots/.config/quickshell/ii/modules/waffle/onScreenDisplay/WaffleOSD.qml @@ -104,13 +104,12 @@ Scope { item: osdIndicatorLoader } - implicitWidth: osdIndicatorLoader.implicitWidth + osdIndicatorLoader.item.visualMargin * 2 - implicitHeight: osdIndicatorLoader.implicitHeight + osdIndicatorLoader.item.visualMargin * 2 + implicitWidth: osdIndicatorLoader.implicitWidth + implicitHeight: osdIndicatorLoader.implicitHeight Loader { id: osdIndicatorLoader anchors.fill: parent - anchors.margins: item.visualMargin source: root.indicators.find(i => i.id === root.currentIndicator)?.sourceUrl Connections { diff --git a/dots/.config/quickshell/ii/modules/waffle/polkit/WPolkitContent.qml b/dots/.config/quickshell/ii/modules/waffle/polkit/WPolkitContent.qml new file mode 100644 index 000000000..1fcb25072 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/polkit/WPolkitContent.qml @@ -0,0 +1,208 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +Rectangle { + id: root + + color: "#000000" + readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true + + Keys.onPressed: event => { // Esc to close + if (event.key === Qt.Key_Escape) { + PolkitService.cancel(); + } + } + + StyledImage { + anchors.fill: parent + source: Config.options.background.wallpaperPath + fillMode: Image.PreserveAspectCrop + + Rectangle { + anchors.fill: parent + color: ColorUtils.transparentize("#000000", 0.31) + + PolkitDialog { + id: dialog + DragHandler { + target: null + property real startX: dialog.x + property real startY: dialog.y + onActiveChanged: { + if (!active) return; + startX = dialog.x; + startY = dialog.y; + } + xAxis.onActiveValueChanged: { + dialog.x = Math.round(startX + xAxis.activeValue); + } + yAxis.onActiveValueChanged: { + dialog.y = Math.round(startY + yAxis.activeValue); + } + } + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + } + } + } + + component PolkitDialog: WPane { + borderColor: Looks.colors.ambientShadow + + contentItem: WPanelPageColumn { + PolkitDialogHeader { + Layout.fillWidth: true + } + BodyRectangle { + id: dialogBody + implicitHeight: bodyContent.implicitHeight + 48 + implicitWidth: 434 + color: Looks.colors.bg1Base + + ColumnLayout { + id: bodyContent + anchors.fill: parent + anchors.margins: 24 + spacing: 20 + + RowLayout { + Layout.fillWidth: true + spacing: 15 + + WAppIcon { + iconName: PolkitService.flow?.iconName ?? "window-shield" + fallback: PolkitService.flow?.iconName == "" ? `${Looks.iconsPath}/window-shield` : PolkitService.flow.iconName + isMask: PolkitService.flow?.iconName === "" + tryCustomIcon: false + } + WText { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + font.pixelSize: Looks.font.pixelSize.larger + font.weight: Looks.font.weight.strongest + text: { + const iconName = PolkitService.flow?.iconName ?? ""; + if (iconName === "") + return Translation.tr("Command-line-invoked Action"); + const desktopEntry = DesktopEntries.applications.values.find(entry => { + return entry.icon == iconName; + }); + return desktopEntry ? desktopEntry.name : Translation.tr("Unknown Application"); + } + } + } + + WText { + Layout.fillWidth: true + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignLeft + text: PolkitService.cleanMessage + } + + WTextField { + id: inputField + Layout.fillWidth: true + focus: true + enabled: PolkitService.interactionAvailable + placeholderText: PolkitService.cleanPrompt + echoMode: root.usePasswordChars ? TextInput.Password : TextInput.Normal + onAccepted: PolkitService.submit(inputField.text) + + Keys.onPressed: event => { // Esc to close + if (event.key === Qt.Key_Escape) { + PolkitService.cancel(); + } + } + + Component.onCompleted: forceActiveFocus() + Connections { + target: PolkitService + function onInteractionAvailableChanged() { + if (!PolkitService.interactionAvailable) + return; + inputField.text = ""; + inputField.forceActiveFocus(); + } + } + } + } + } + BodyRectangle { + implicitHeight: 80 + color: Looks.colors.bgPanelFooterBackground + RowLayout { + anchors.fill: parent + anchors.margins: 24 + spacing: 8 + uniformCellSizes: true + + WButton { + Layout.fillWidth: true + implicitHeight: 32 + colBackground: Looks.colors.bg1 + horizontalAlignment: Text.AlignHCenter + text: Translation.tr("Yes") + onClicked: PolkitService.submit(inputField.text) + } + WButton { + Layout.fillWidth: true + implicitHeight: 32 + horizontalAlignment: Text.AlignHCenter + checked: true + text: Translation.tr("No") + onClicked: PolkitService.cancel() + } + } + } + } + } + + component PolkitDialogHeader: BodyRectangle { + implicitHeight: headerContent.implicitHeight + color: Looks.colors.bg2Base + + CloseButton { + anchors { + top: parent.top + right: parent.right + } + radius: 0 + implicitWidth: 32 + implicitHeight: 32 + + onClicked: { + PolkitService.cancel(); + } + } + + ColumnLayout { + id: headerContent + anchors.fill: parent + anchors.leftMargin: 24 + anchors.rightMargin: 24 + spacing: 18 + + WText { + Layout.topMargin: 20 + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + text: Translation.tr("Polkit") + } + WText { + Layout.fillWidth: true + Layout.bottomMargin: 12 + horizontalAlignment: Text.AlignLeft + wrapMode: Text.Wrap + text: Translation.tr("Do you want to allow this app to make changes to your device?") + font.pixelSize: Looks.font.pixelSize.xlarger + font.weight: Looks.font.weight.strongest + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/polkit/WafflePolkit.qml b/dots/.config/quickshell/ii/modules/waffle/polkit/WafflePolkit.qml new file mode 100644 index 000000000..8aa7d5672 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/polkit/WafflePolkit.qml @@ -0,0 +1,15 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import QtQuick +import Quickshell +import Quickshell.Wayland + +FullscreenPolkitWindow { + id: root + contentComponent: Component { + WPolkitContent {} + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/screenSnip/WRectangularSelection.qml b/dots/.config/quickshell/ii/modules/waffle/screenSnip/WRectangularSelection.qml new file mode 100644 index 000000000..1de501613 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/screenSnip/WRectangularSelection.qml @@ -0,0 +1,62 @@ +import QtQuick +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +Item { + id: root + + required property int regionX + required property int regionY + required property int regionWidth + required property int regionHeight + + property bool dashed: true + property color borderColor: "#ffffff" + property color overlayColor: ColorUtils.transparentize("#000000", 1) + Component.onCompleted: overlayColor = ColorUtils.transparentize("#000000", 0.4) + Behavior on overlayColor { + ColorAnimation { + duration: 150 + easing.type: Easing.InOutQuad + } + } + + // Overlay to darken screen + // Base dark overlay around region + Rectangle { + id: darkenOverlay + z: 1 + anchors { + left: parent.left + top: parent.top + leftMargin: root.regionX - darkenOverlay.border.width + topMargin: root.regionY - darkenOverlay.border.width + } + width: root.regionWidth + darkenOverlay.border.width * 2 + height: root.regionHeight + darkenOverlay.border.width * 2 + color: "transparent" + border.color: root.overlayColor + border.width: Math.max(root.width, root.height) + } + + // Selection border + DashedBorder { + id: border + z: 2 + visible: root.regionWidth > 0 && root.regionHeight > 0 + anchors { + left: parent.left + top: parent.top + leftMargin: Math.round(root.regionX - borderWidth) + topMargin: Math.round(root.regionY - borderWidth) + } + width: Math.round(root.regionWidth + borderWidth * 2) + height: Math.round(root.regionHeight + borderWidth * 2) + color: root.borderColor + dashLength: 4 + gapLength: root.dashed ? 3 : 0 + borderWidth: 1 + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/screenSnip/WRegionSelectionPanel.qml b/dots/.config/quickshell/ii/modules/waffle/screenSnip/WRegionSelectionPanel.qml new file mode 100644 index 000000000..318887928 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/screenSnip/WRegionSelectionPanel.qml @@ -0,0 +1,375 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt.labs.synchronizer +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.utils +import qs.modules.common.widgets +import qs.modules.waffle.looks + +PanelWindow { + id: root + + enum MediaType { + Image, + Video + } + enum ImageAction { + Copy, + Menu, + CharRecognition, + Search + } + enum VideoAction { + Record, + RecordWithSound + } + enum SelectionMode { + Rect, + Window + } + + signal closed + function close() { + root.closed(); + } + + property var mediaType: WRegionSelectionPanel.MediaType.Image + property var imageAction: WRegionSelectionPanel.ImageAction.Copy + property var selectionMode: WRegionSelectionPanel.SelectionMode.Rect + + visible: false + color: "transparent" + WlrLayershell.namespace: "quickshell:regionSelector" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + exclusionMode: ExclusionMode.Ignore + anchors { + left: true + right: true + top: true + bottom: true + } + + // Hyprland stuff + readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen) + readonly property real monitorScale: hyprlandMonitor.scale + readonly property var windows: [...HyprlandData.windowList].sort((a, b) => { + // Sort floating=true windows before others + if (a.floating === b.floating) + return 0; + return a.floating ? -1 : 1; + }) + + property string screenshotDir: Directories.screenshotTemp + property string screenshotPath: `${root.screenshotDir}/image-${screen.name}` + TempScreenshotProcess { + id: screenshotProc + running: true + screen: root.screen + screenshotDir: root.screenshotDir + screenshotPath: root.screenshotPath + onExited: (exitCode, exitStatus) => { + root.preparationDone = true; + } + } + property bool preparationDone: false + onPreparationDoneChanged: { + if (!preparationDone) + return; + root.visible = true; + } + + function getScreenshotAction() { + switch (root.mediaType) { + case WRegionSelectionPanel.MediaType.Image: + switch (root.imageAction) { + case WRegionSelectionPanel.ImageAction.Copy: + return ScreenshotAction.Action.Copy; + case WRegionSelectionPanel.ImageAction.Menu: + return ScreenshotAction.Action.Edit; + case WRegionSelectionPanel.ImageAction.CharRecognition: + return ScreenshotAction.Action.CharRecognition; + case WRegionSelectionPanel.ImageAction.Search: + return ScreenshotAction.Action.Search; + default: + return ScreenshotAction.Action.Copy; + } + break; + case WRegionSelectionPanel.MediaType.Video: + switch (root.videoAction) { + case WRegionSelectionPanel.VideoAction.Record: + return ScreenshotAction.Action.Record; + case WRegionSelectionPanel.VideoAction.RecordWithSound: + return ScreenshotAction.Action.RecordWithSound; + } + } + } + + Process { + id: snipProc + } + + ScreencopyView { + id: screencopyView + anchors.fill: parent + live: false + captureSource: root.screen + + focus: root.visible + Keys.onPressed: event => { // Esc to close + if (event.key === Qt.Key_Escape) { + root.close(); + } else if (event.key === Qt.Key_E && event.modifiers & Qt.ControlModifier) { + if (root.imageAction === WRegionSelectionPanel.ImageAction.Menu) { + root.imageAction = WRegionSelectionPanel.ImageAction.Copy; + } else { + root.imageAction = WRegionSelectionPanel.ImageAction.Menu; + } + } + } + + DragManager { + id: dragArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.CrossCursor + + property bool isWindowSelection: root.selectionMode === WRegionSelectionPanel.SelectionMode.Window + property var hoveredWindow: root.windows.find(w => { + const inCurrentWorkspace = w.workspace.id === HyprlandData.activeWorkspace.id; + const withinXRange = w.at[0] <= dragArea.mouseX && dragArea.mouseX <= w.at[0] + w.size[0]; + const withinYRange = w.at[1] <= dragArea.mouseY && dragArea.mouseY <= w.at[1] + w.size[1]; + return inCurrentWorkspace && withinXRange && withinYRange; + }) + property int winPadding: 1 + property int selectionX: isWindowSelection ? ((hoveredWindow?.at[0] ?? 0) - winPadding) : regionTopLeftX + property int selectionY: isWindowSelection ? ((hoveredWindow?.at[1] ?? 0) - winPadding) : regionTopLeftY + property int selectionWidth: isWindowSelection ? ((hoveredWindow?.size[0] ?? 0) + winPadding * 2) : regionWidth + property int selectionHeight: isWindowSelection ? ((hoveredWindow?.size[1] ?? 0) + winPadding * 2) : regionHeight + + onDragReleased: (diffX, diffY) => { + if (selectionWidth === 0 || selectionHeight === 0) { + return; + } + const screenshotDir = Config.options.screenSnip.savePath !== "" ? Config.options.screenSnip.savePath : ""; + const screenshotAction = root.getScreenshotAction(); + const command = ScreenshotAction.getCommand(dragArea.selectionX * root.monitorScale // + , dragArea.selectionY * root.monitorScale // + , dragArea.selectionWidth * root.monitorScale// + , dragArea.selectionHeight * root.monitorScale // + , root.screenshotPath // + , screenshotAction // + , screenshotDir); // yo wtf is this formatting qmlls do be funnie + snipProc.command = command; + + // Image post-processing + snipProc.startDetached(); + root.close(); + } + + WRectangularSelection { + id: rectangularSelection + anchors.fill: parent + regionX: dragArea.selectionX + regionY: dragArea.selectionY + regionWidth: dragArea.selectionWidth + regionHeight: dragArea.selectionHeight + dashed: root.selectionMode === WRegionSelectionPanel.SelectionMode.Rect + } + + RegionSelectionOptionsToolbar { + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: 12 + } + } + } + } + + component RegionSelectionOptionsToolbar: WToolbar { + // Image/video + WToolbarTabBar { + currentIndex: switch (root.mediaType) { + case WRegionSelectionPanel.MediaType.Image: + return 0; + case WRegionSelectionPanel.MediaType.Video: + return 1; + default: + return 0; + } + WToolbarIconTabButton { + icon.name: "camera" + icon.color: Looks.colors.fg + } + WToolbarIconTabButton { + icon.name: "video" + icon.color: Looks.colors.fg + } + onCurrentIndexChanged: { + switch (currentIndex) { + case 0: + root.mediaType = WRegionSelectionPanel.MediaType.Image; + break; + case 1: + root.mediaType = WRegionSelectionPanel.MediaType.Video; + break; + } + } + + WToolTip { + text: Translation.tr("Snip") + } + } + + // Selection type + WToolbarButton { + id: selectionTypeBtn + implicitWidth: selectionTypeBtnRow.implicitWidth + 11 * 2 + leftPadding: 11 + rightPadding: 11 + onClicked: { + selectionTypeMenu.visible = !selectionTypeMenu.visible; + } + contentItem: Row { + id: selectionTypeBtnRow + spacing: 4 + FluentIcon { + anchors.verticalCenter: parent.verticalCenter + icon: switch (root.selectionMode) { + case WRegionSelectionPanel.SelectionMode.Rect: + return "crop"; + case WRegionSelectionPanel.SelectionMode.Window: + return "calendar-add"; + default: + return "crop"; + } + implicitSize: 18 + } + FluentIcon { + anchors { + top: parent.top + topMargin: (parent.height - height) / 2 + (selectionTypeBtn.down ? 2 : 0) + Behavior on topMargin { + animation: Looks.transition.enter.createObject(this) + } + } + icon: "chevron-down" + implicitSize: 12 + } + } + + WMenu { + id: selectionTypeMenu + onClosed: screencopyView.focus = true + x: -margins + y: -margins - (selectionTypeBtn.parent.height - selectionTypeBtn.height) - 16 + topMargin: -6 + height: implicitHeight + sourceEdgeMargin + + color: Looks.colors.bg1Base + + Action { + icon.name: "crop" + text: Translation.tr("Rectangle") + checked: root.selectionMode === WRegionSelectionPanel.SelectionMode.Rect + onTriggered: { + root.selectionMode = WRegionSelectionPanel.SelectionMode.Rect; + } + } + Action { + icon.name: "calendar-add" + text: Translation.tr("Window") + checked: root.selectionMode === WRegionSelectionPanel.SelectionMode.Window + onTriggered: { + root.selectionMode = WRegionSelectionPanel.SelectionMode.Window; + } + } + } + + WToolTip { + text: Translation.tr("Snipping area") + } + } + + // Markup + WToolbarIconButton { + icon.name: "image-edit" + enabled: root.mediaType === WRegionSelectionPanel.MediaType.Image + checked: root.imageAction === WRegionSelectionPanel.ImageAction.Menu + onClicked: { + if (root.imageAction === WRegionSelectionPanel.ImageAction.Menu) { + root.imageAction = WRegionSelectionPanel.ImageAction.Copy; + } else { + root.imageAction = WRegionSelectionPanel.ImageAction.Menu; + } + } + WToolTip { + text: Translation.tr("Quick markup (Ctrl+E)") + } + } + + WToolbarSeparator {} + + // Tools + WToolbarIconButton { + icon.name: "search-visual" + checked: root.imageAction === WRegionSelectionPanel.ImageAction.Search + onClicked: { + if (root.imageAction === WRegionSelectionPanel.ImageAction.Search && root.mediaType === WRegionSelectionPanel.MediaType.Image) { + root.imageAction = WRegionSelectionPanel.ImageAction.Copy; + } else { + root.mediaType = WRegionSelectionPanel.MediaType.Image; + root.imageAction = WRegionSelectionPanel.ImageAction.Search; + } + } + WToolTip { + text: Translation.tr("Image search") + } + } + WToolbarIconButton { + icon.name: "eyedropper" + onClicked: { + Quickshell.execDetached(["bash", "-c", "sleep 0.2; hyprpicker -a"]); + root.closed(); + } + WToolTip { + text: Translation.tr("Color picker") + } + } + WToolbarIconButton { + icon.name: "scan-text" + checked: root.imageAction === WRegionSelectionPanel.ImageAction.CharRecognition + onClicked: { + if (root.imageAction === WRegionSelectionPanel.ImageAction.CharRecognition && root.mediaType === WRegionSelectionPanel.MediaType.Image) { + root.imageAction = WRegionSelectionPanel.ImageAction.Copy; + } else { + root.mediaType = WRegionSelectionPanel.MediaType.Image; + root.imageAction = WRegionSelectionPanel.ImageAction.CharRecognition; + } + } + WToolTip { + text: Translation.tr("Text extractor") + } + } + + WToolbarSeparator {} + + WToolbarIconButton { + icon.name: "dismiss" + onClicked: root.close() + WToolTip { + text: Translation.tr("Close (Esc)") + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/screenSnip/WScreenSnip.qml b/dots/.config/quickshell/ii/modules/waffle/screenSnip/WScreenSnip.qml new file mode 100644 index 000000000..c462f9e7b --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/screenSnip/WScreenSnip.qml @@ -0,0 +1,106 @@ +pragma ComponentBehavior: Bound +import qs +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.services +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import Quickshell.Hyprland + +Scope { + id: root + + function dismiss() { + GlobalStates.regionSelectorOpen = false; + } + + Loader { + id: regionSelectorLoader + active: GlobalStates.regionSelectorOpen + + sourceComponent: WRegionSelectionPanel { + onClosed: root.dismiss() + } + } + + function screenshot() { + GlobalStates.regionSelectorOpen = true; + } + + function ocr() { + GlobalStates.regionSelectorOpen = true; + regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Image; + regionSelectorLoader.item.imageAction = WRegionSelectionPanel.ImageAction.CharRecognition; + } + + function record() { + GlobalStates.regionSelectorOpen = true; + regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Video; + regionSelectorLoader.item.videoAction = WRegionSelectionPanel.VideoAction.Record; + } + + function recordWithSound() { + GlobalStates.regionSelectorOpen = true; + regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Video; + regionSelectorLoader.item.videoAction = WRegionSelectionPanel.VideoAction.RecordWithSound; + } + + function search() { + GlobalStates.regionSelectorOpen = true; + regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Image; + regionSelectorLoader.item.imageAction = WRegionSelectionPanel.ImageAction.Search; + } + + IpcHandler { + target: "region" + + function screenshot() { + root.screenshot(); + } + function ocr() { + root.ocr(); + } + function record() { + root.record(); + } + function recordWithSound() { + root.recordWithSound(); + } + function search() { + root.search(); + } + } + + GlobalShortcut { + name: "regionScreenshot" + description: "Takes a screenshot of the selected region" + onPressed: root.screenshot() + } + GlobalShortcut { + name: "regionSearch" + description: "Searches the selected region" + onPressed: root.search() + } + GlobalShortcut { + name: "regionOcr" + description: "Recognizes text in the selected region" + onPressed: root.ocr() + } + GlobalShortcut { + name: "regionRecord" + description: "Records the selected region" + onPressed: root.record() + } + GlobalShortcut { + name: "regionRecordWithSound" + description: "Records the selected region with sound" + onPressed: root.recordWithSound() + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/sessionScreen/PowerButton.qml b/dots/.config/quickshell/ii/modules/waffle/sessionScreen/PowerButton.qml new file mode 100644 index 000000000..d3bd07ad3 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/sessionScreen/PowerButton.qml @@ -0,0 +1,76 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io + +WSessionScreenTextButton { + id: root + implicitWidth: 40 + implicitHeight: 40 + focusRingRadius: Looks.radius.large + colBackground: ColorUtils.transparentize(Looks.darkColors.bg2) + colBackgroundHover: Looks.applyContentTransparency(Looks.darkColors.bg2Hover) + colBackgroundActive: Looks.applyContentTransparency(Looks.darkColors.bg2Active) + property color color: { + if (root.down) { + return root.colBackgroundActive; + } else if (root.hovered) { + return root.colBackgroundHover; + } else { + return root.colBackground; + } + } + background: Rectangle { + id: background + radius: Looks.radius.medium + color: root.color + } + contentItem: Item { + FluentIcon { + anchors.centerIn: parent + implicitSize: 20 + icon: "power" + color: root.fgColor + } + } + + onClicked: { + powerMenu.visible = !powerMenu.visible; + } + + WMenu { + id: powerMenu + x: -powerMenu.implicitWidth / 2 + root.implicitWidth / 2 + y: -powerMenu.implicitHeight + + color: Looks.darkColors.bg1Base + Component.onCompleted: { + powerMenu.backgroundPane.borderColor = Looks.applyContentTransparency(Looks.darkColors.bg2Border); + } + delegate: WMenuItem { + id: menuItemDelegate + colBackground: ColorUtils.transparentize(Looks.darkColors.bg1Base) + colBackgroundHover: Looks.applyContentTransparency(Looks.darkColors.bg2Hover) + colBackgroundActive: Looks.applyContentTransparency(Looks.darkColors.bg2Active) + colForeground: Looks.darkColors.fg + } + + Action { + icon.name: "power" + text: Translation.tr("Shut down") + onTriggered: Session.poweroff() + } + Action { + icon.name: "arrow-counterclockwise" + text: Translation.tr("Restart") + onTriggered: Session.reboot() + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/sessionScreen/SessionScreenContent.qml b/dots/.config/quickshell/ii/modules/waffle/sessionScreen/SessionScreenContent.qml new file mode 100644 index 000000000..6a9d0ef31 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/sessionScreen/SessionScreenContent.qml @@ -0,0 +1,139 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell + +Item { + id: root + + Component.onCompleted: { + lockButton.forceActiveFocus(); + } + + ColumnLayout { + anchors.centerIn: parent + spacing: 4 + + WSessionScreenTextButton { + id: lockButton + focus: true + text: Translation.tr("Lock") + onClicked: { + GlobalStates.sessionOpen = false; + Session.lock(); + } + KeyNavigation.up: powerButton + KeyNavigation.down: signOutButton + } + WSessionScreenTextButton { + id: signOutButton + focus: true + text: Translation.tr("Sign out") + onClicked: { + GlobalStates.sessionOpen = false; + Session.logout(); + } + KeyNavigation.up: lockButton + KeyNavigation.down: changePasswordButton + } + + WSessionScreenTextButton { + id: changePasswordButton + focus: true + text: Translation.tr("Change password") + onClicked: { + GlobalStates.sessionOpen = false; + Session.changePassword(); + } + KeyNavigation.up: signOutButton + KeyNavigation.down: taskManagerButton + } + + WSessionScreenTextButton { + id: taskManagerButton + focus: true + text: Translation.tr("Task Manager") + onClicked: { + GlobalStates.sessionOpen = false; + Session.launchTaskManager(); + } + KeyNavigation.up: signOutButton + KeyNavigation.down: cancelButton + } + + CancelButton { + id: cancelButton + Layout.fillWidth: true + Layout.leftMargin: 5 + Layout.rightMargin: 5 + Layout.topMargin: 38 + onClicked: GlobalStates.sessionOpen = false + KeyNavigation.up: taskManagerButton + KeyNavigation.down: powerButton + } + } + + RowLayout { + anchors { + bottom: parent.bottom + right: parent.right + bottomMargin: 21 + rightMargin: 31 + } + PowerButton { + id: powerButton + KeyNavigation.up: cancelButton + KeyNavigation.down: lockButton + } + } + + component CancelButton: WBorderlessButton { + id: root + implicitHeight: 32 + colBackground: Looks.darkColors.bg1Base + colBackgroundHover: Qt.lighter(Looks.darkColors.bg1Base, 1.2) + colBackgroundActive: Qt.lighter(Looks.darkColors.bg1Base, 1.1) + colForeground: Looks.darkColors.fg + + property bool keyboardDown: false + + Keys.onPressed: event => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + keyboardDown = true; + event.accepted = true; + } + } + Keys.onReleased: event => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + keyboardDown = false; + root.clicked(); + event.accepted = true; + } + } + + contentItem: WText { + text: Translation.tr("Cancel") + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Looks.font.pixelSize.large + color: root.colForeground + } + + Rectangle { + visible: cancelButton.focus + anchors { + fill: parent + margins: -3 + } + radius: cancelButton.background.radius + 4 + color: "transparent" + border.width: 2 + border.color: "#ffffff" + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/sessionScreen/WSessionScreenTextButton.qml b/dots/.config/quickshell/ii/modules/waffle/sessionScreen/WSessionScreenTextButton.qml new file mode 100644 index 000000000..875ca03b4 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/sessionScreen/WSessionScreenTextButton.qml @@ -0,0 +1,55 @@ +pragma ComponentBehavior: Bound +import QtQuick +import qs +import qs.modules.waffle.looks + +WTextButton { + id: root + + implicitWidth: 135 + implicitHeight: 40 + horizontalPadding: 5 + + property bool keyboardDown: false + property alias focusRingRadius: focusRing.radius + fgColor: (root.pressed || root.keyboardDown) ? Looks.darkColors.fg1 : Looks.darkColors.fg + + Keys.onPressed: event => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + keyboardDown = true; + event.accepted = true; + } + } + Keys.onReleased: event => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + keyboardDown = false; + root.clicked(); + event.accepted = true; + } + } + + contentItem: Item { + id: contentItem + implicitWidth: buttonText.implicitWidth + + WText { + id: buttonText + anchors.fill: parent + color: root.fgColor + text: root.text + font.pixelSize: Looks.font.pixelSize.large + } + } + + Rectangle { + id: focusRing + visible: root.focus + anchors { + fill: parent + margins: -4 + } + color: "transparent" + border.width: 2 + border.color: "#ffffff" + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/sessionScreen/WaffleSessionScreen.qml b/dots/.config/quickshell/ii/modules/waffle/sessionScreen/WaffleSessionScreen.qml new file mode 100644 index 000000000..7f0d1a5d7 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/sessionScreen/WaffleSessionScreen.qml @@ -0,0 +1,116 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +Scope { + id: root + property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) + + Loader { + id: sessionLoader + active: GlobalStates.sessionOpen + onActiveChanged: { + if (sessionLoader.active) SessionWarnings.refresh(); + } + + Connections { + target: GlobalStates + function onScreenLockedChanged() { + if (GlobalStates.screenLocked) { + GlobalStates.sessionOpen = false; + } + } + } + + sourceComponent: PanelWindow { // Session menu + id: sessionRoot + visible: sessionLoader.active + property string subtitle + + function hide() { + GlobalStates.sessionOpen = false; + } + + exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell:session" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + // This is a big surface so we needa carefully choose the transparency, + // or we'll get a large scary rgb blob + color: "#000000" + + anchors { + top: true + left: true + right: true + bottom: true + } + + Item { + anchors.fill: parent + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + sessionRoot.hide(); + } + } + + SessionScreenContent { + anchors.fill: parent + } + } + } + } + + IpcHandler { + target: "session" + + function toggle(): void { + GlobalStates.sessionOpen = !GlobalStates.sessionOpen; + } + + function close(): void { + GlobalStates.sessionOpen = false + } + + function open(): void { + GlobalStates.sessionOpen = true + } + } + + GlobalShortcut { + name: "sessionToggle" + description: "Toggles session screen on press" + + onPressed: { + GlobalStates.sessionOpen = !GlobalStates.sessionOpen; + } + } + + GlobalShortcut { + name: "sessionOpen" + description: "Opens session screen on press" + + onPressed: { + GlobalStates.sessionOpen = true + } + } + + GlobalShortcut { + name: "sessionClose" + description: "Closes session screen on press" + + onPressed: { + GlobalStates.sessionOpen = false + } + } + +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/SearchBar.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/SearchBar.qml new file mode 100644 index 000000000..d38bc8d98 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/SearchBar.qml @@ -0,0 +1,104 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +FooterRectangle { + id: root + + property real horizontalPadding: 32 + property real verticalPadding: 16 + property bool searching: text.length > 0 + property alias searchInput: searchInput + property alias text: searchInput.text + implicitHeight: outline.implicitHeight + verticalPadding * 2 + + signal accepted() + + Component.onCompleted: forceFocus() + function forceFocus() { + searchInput.forceActiveFocus(); + } + + focus: true + color: searching ? Looks.colors.bgPanelBody : Looks.colors.bgPanelFooter + + Behavior on horizontalPadding { + enabled: Config.options.waffles.tweaks.smootherSearchBar + animation: Looks.transition.move.createObject(this) + } + Behavior on verticalPadding { + enabled: Config.options.waffles.tweaks.smootherSearchBar + animation: Looks.transition.move.createObject(this) + } + + Rectangle { + id: outline + anchors { + left: parent.left + right: parent.right + leftMargin: root.horizontalPadding + rightMargin: root.horizontalPadding + verticalCenter: parent.verticalCenter + } + implicitHeight: 32 + color: "transparent" + radius: height / 2 + border.width: 1 + border.color: Looks.colors.bg2Border + } + + Rectangle { + id: searchInputBg + anchors.fill: outline + anchors.margins: 1 + radius: height / 2 + color: Looks.colors.inputBg + + RowLayout { + anchors.fill: parent + spacing: 11 + + WAppIcon { + Layout.leftMargin: 14 + iconName: "system-search-checked" + separateLightDark: true + implicitSize: 18 + } + + WTextInput { + id: searchInput + focus: true + Layout.fillWidth: true + + WText { + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + color: Looks.colors.accentUnfocused + text: Translation.tr("Search for apps") // should also have "", settings, and documents" but we don't have those + visible: searchInput.text.length === 0 + font.pixelSize: Looks.font.pixelSize.large + } + + onAccepted: { + root.accepted(); + } + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/StartMenuContent.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/StartMenuContent.qml new file mode 100644 index 000000000..64ad321c6 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/StartMenuContent.qml @@ -0,0 +1,126 @@ +pragma ComponentBehavior: Bound +import Qt.labs.synchronizer +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks +import qs.modules.waffle.startMenu.startPage +import qs.modules.waffle.startMenu.searchPage + +WBarAttachedPanelContent { + id: root + + property bool searching: false + property string searchText: LauncherSearch.query + + StartMenuContext { + id: context + } + + Keys.onPressed: event => { + // Prevent Esc and Backspace from registering + if (event.key === Qt.Key_Escape) + return; + + // Handle Backspace: focus and delete character if not focused + if (event.key === Qt.Key_Backspace) { + searchBar.forceFocus(); + if (event.modifiers & Qt.ControlModifier) { + // Delete word before cursor + let text = searchBar.text; + let pos = searchBar.searchInput.cursorPosition; + if (pos > 0) { + // Find the start of the previous word + let left = text.slice(0, pos); + let match = left.match(/(\s*\S+)\s*$/); + let deleteLen = match ? match[0].length : 1; + searchBar.text = text.slice(0, pos - deleteLen) + text.slice(pos); + searchBar.searchInput.cursorPosition = pos - deleteLen; + } + } else { + // Delete character before cursor if any + if (searchBar.searchInput.cursorPosition > 0) { + searchBar.text = searchBar.text.slice(0, searchBar.searchInput.cursorPosition - 1) + searchBar.text.slice(searchBar.searchInput.cursorPosition); + searchBar.searchInput.cursorPosition -= 1; + } + } + // Always move cursor to end after programmatic edit + searchBar.searchInput.cursorPosition = searchBar.text.length; + event.accepted = true; + // If already focused, let TextField handle it + return; + } + + // Only handle visible printable characters (ignore control chars, arrows, etc.) + if (event.text && event.text.length === 1 && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && event.key !== Qt.Key_Delete && event.text.charCodeAt(0) >= 0x20) // ignore control chars like Backspace, Tab, etc. + { + if (!searchBar.searchInput.activeFocus) { + searchBar.forceFocus(); + // Insert the character at the cursor position + searchBar.text = searchBar.text.slice(0, searchBar.searchInput.cursorPosition) + event.text + searchBar.text.slice(searchBar.searchInput.cursorPosition); + searchBar.searchInput.cursorPosition += 1; + event.accepted = true; + context.setCurrentIndex(0); + } + } + + // Arrow keys for item navigation + if (event.key === Qt.Key_Down) { + let maxIndex = Math.max(0, LauncherSearch.results.length - 1); + context.setCurrentIndex(Math.min(context.currentIndex + 1, maxIndex)); + event.accepted = true; + } else if (event.key === Qt.Key_Up) { + context.setCurrentIndex(Math.max(context.currentIndex - 1, 0)); + event.accepted = true; + } + } + + contentItem: WPane { + contentItem: WPanelPageColumn { + SearchBar { + id: searchBar + Layout.fillWidth: true + implicitWidth: 832 // TODO: Make sizes naturally inferred + horizontalPadding: 32 + // verticalPadding: root.searching ? 32 : 16 // TODO: make this not nuke the panel + Synchronizer on searching { + property alias target: root.searching + } + focus: true + text: root.searchText + onTextChanged: { + LauncherSearch.query = text; + } + onAccepted: { + context.accepted(); + } + } + Item { + implicitHeight: root.searching ? 800 : 800 // TODO: Make sizes naturally inferred + Layout.fillWidth: true + Loader { + id: pageContentLoader + anchors.fill: parent + sourceComponent: root.searching ? searchPageComp : startPageComp + } + } + } + } + + Component { + id: searchPageComp + SearchPageContent { + context: context + } + } + + Component { + id: startPageComp + StartPageContent {} + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/StartMenuContext.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/StartMenuContext.qml new file mode 100644 index 000000000..ff2289da0 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/StartMenuContext.qml @@ -0,0 +1,64 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs +import qs.modules.common +import qs.services + +Scope { + id: root + + signal accepted + + property int currentIndex: 0 + function setCurrentIndex(index) { + if (index == currentIndex) + return; + currentIndex = index; + } + + function selectCategory(category) { + for (let i = 0; i < root.categories.length; i++) { + const thisCategoryName = root.categories[i].name; + if (thisCategoryName.startsWith(category) || category.startsWith(thisCategoryName)) { + LauncherSearch.ensurePrefix(root.categories[i].prefix); + return; + } + } + } + property list categories: [ + { + name: Translation.tr("All"), + prefix: "" + }, + { + name: Translation.tr("Apps"), + prefix: Config.options.search.prefix.app + }, + { + name: Translation.tr("Actions"), + prefix: Config.options.search.prefix.action + }, + { + name: Translation.tr("Clipboard"), + prefix: Config.options.search.prefix.clipboard + }, + { + name: Translation.tr("Emojis"), + prefix: Config.options.search.prefix.emojis + }, + { + name: Translation.tr("Math"), + prefix: Config.options.search.prefix.math + }, + { + name: Translation.tr("Commands"), + prefix: Config.options.search.prefix.shellCommand + }, + { + name: Translation.tr("Web"), + prefix: Config.options.search.prefix.webSearch + }, + ] + +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/WaffleStartMenu.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/WaffleStartMenu.qml new file mode 100644 index 000000000..62bdc65bf --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/WaffleStartMenu.qml @@ -0,0 +1,153 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets + +Scope { + id: root + + Connections { + target: GlobalStates + + function onSearchOpenChanged() { + if (GlobalStates.searchOpen) { + LauncherSearch.query = ""; + panelLoader.active = true; + } + } + } + + Loader { + id: panelLoader + active: GlobalStates.searchOpen + sourceComponent: PanelWindow { + id: panelWindow + exclusiveZone: 0 + WlrLayershell.namespace: "quickshell:wStartMenu" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + color: "transparent" + + anchors { + bottom: Config.options.waffles.bar.bottom + top: !Config.options.waffles.bar.bottom + left: Config.options.waffles.bar.leftAlignApps + } + + implicitWidth: content.implicitWidth + implicitHeight: content.implicitHeight + + HyprlandFocusGrab { + id: focusGrab + active: true + windows: [panelWindow] + onCleared: content.close() + } + + Connections { + target: GlobalStates + function onSearchOpenChanged() { + if (!GlobalStates.searchOpen) + content.close(); + } + } + + StartMenuContent { + id: content + anchors.fill: parent + focus: true + + onClosed: { + GlobalStates.searchOpen = false; + panelLoader.active = false; + LauncherSearch.query = ""; + } + } + } + } + + function toggleClipboard() { + if (LauncherSearch.query.startsWith(Config.options.search.prefix.clipboard) || !GlobalStates.searchOpen) { + GlobalStates.searchOpen = !GlobalStates.searchOpen; + } + LauncherSearch.ensurePrefix(Config.options.search.prefix.clipboard); + } + function toggleEmojis() { + if (LauncherSearch.query.startsWith(Config.options.search.prefix.emojis) || !GlobalStates.searchOpen) { + GlobalStates.searchOpen = !GlobalStates.searchOpen; + } + LauncherSearch.ensurePrefix(Config.options.search.prefix.emojis); + } + + IpcHandler { + target: "search" + + function toggle() { + GlobalStates.searchOpen = !GlobalStates.searchOpen; + } + function close() { + GlobalStates.searchOpen = false; + } + function open() { + GlobalStates.searchOpen = true; + } + function toggleReleaseInterrupt() { + GlobalStates.superReleaseMightTrigger = false; + } + } + + GlobalShortcut { + name: "searchToggle" + description: "Toggles search on press" + + onPressed: { + GlobalStates.searchOpen = !GlobalStates.searchOpen; + } + } + GlobalShortcut { + name: "searchToggleRelease" + description: "Toggles search on release" + + onPressed: { + GlobalStates.superReleaseMightTrigger = true; + } + + onReleased: { + if (!GlobalStates.superReleaseMightTrigger) { + GlobalStates.superReleaseMightTrigger = true; + return; + } + GlobalStates.searchOpen = !GlobalStates.searchOpen; + } + } + GlobalShortcut { + name: "searchToggleReleaseInterrupt" + description: "Interrupts possibility of search being toggled on release. " + "This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. " + "To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything." + + onPressed: { + GlobalStates.superReleaseMightTrigger = false; + } + } + + GlobalShortcut { + name: "overviewClipboardToggle" + description: "Toggle clipboard query on overview widget" + + onPressed: { + root.toggleClipboard(); + } + } + + GlobalShortcut { + name: "overviewEmojiToggle" + description: "Toggle emoji query on overview widget" + + onPressed: { + root.toggleEmojis(); + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchEntryIcon.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchEntryIcon.qml new file mode 100644 index 000000000..5f94870b7 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchEntryIcon.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.models +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +Item { + id: root + required property LauncherSearchResult entry + property int iconSize: 24 + implicitWidth: Math.max(iconSize, textIconLoader.implicitWidth) + implicitHeight: iconSize + Loader { + anchors.centerIn: parent + active: root.entry.iconType === LauncherSearchResult.IconType.System && root.entry.iconName !== "" + sourceComponent: WAppIcon { + implicitSize: root.iconSize + iconName: root.entry.iconName + tryCustomIcon: false + animated: false + } + } + Loader { + id: textIconLoader + anchors.centerIn: parent + active: root.entry.iconType === LauncherSearchResult.IconType.Text + sourceComponent: WText { + text: root.entry.iconName + font.pixelSize: root.iconSize + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + Loader { + anchors.centerIn: parent + active: root.entry.iconType === LauncherSearchResult.IconType.Material || root.entry.iconType === LauncherSearchResult.IconType.None || root.entry.iconName === "" + sourceComponent: FluentIcon { + icon: root.entry.iconName ? WIcons.fluentFromMaterial(root.entry.iconName) : WIcons.guessIconForName(root.entry.name) + implicitSize: root.iconSize + animated: false + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchPageContent.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchPageContent.qml new file mode 100644 index 000000000..b6f4a4e94 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchPageContent.qml @@ -0,0 +1,40 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +BodyRectangle { + id: root + + property alias context: searchResults.context + property string searchText: LauncherSearch.query + property alias currentIndex: searchResults.currentIndex + + ColumnLayout { + anchors { + fill: parent + topMargin: 2 + leftMargin: 24 + rightMargin: 24 + } + spacing: 12 + + TagStrip { + context: root.context + Layout.fillWidth: true + Layout.fillHeight: false + } + + SearchResults { + id: searchResults + Layout.fillWidth: true + Layout.fillHeight: true + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchResultButton.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchResultButton.qml new file mode 100644 index 000000000..db45480f4 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchResultButton.qml @@ -0,0 +1,124 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.models +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +WChoiceButton { + id: root + + required property LauncherSearchResult entry + property bool firstEntry: false + + signal requestFocus() + + checked: focus + animateChoiceHighlight: false + implicitWidth: contentLayout.implicitWidth + leftPadding + rightPadding + implicitHeight: contentLayout.implicitHeight + topPadding + bottomPadding + + onClicked: { + execute(); + } + + function execute() { + GlobalStates.searchOpen = false; + root.entry.execute(); + } + + horizontalPadding: 0 + verticalPadding: 0 + + contentItem: RowLayout { + id: contentLayout + spacing: 0 + + WButton { + id: launchButton + Layout.fillWidth: true + Layout.fillHeight: true + horizontalPadding: 10 + verticalPadding: 11 + implicitHeight: Math.max(root.firstEntry ? 62 : 36, entryContentRow.implicitHeight + 8 * 2) + implicitWidth: entryContentRow.implicitWidth + leftPadding + rightPadding + topRightRadius: 0 + bottomRightRadius: 0 + onClicked: root.click() + contentItem: Item { + RowLayout { + id: entryContentRow + anchors { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + } + spacing: 8 + + SearchEntryIcon { + entry: root.entry + iconSize: 24 + } + EntryNameColumn { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + } + } + } + Rectangle { + id: separator + opacity: (root.hovered && !root.checked) ? 1 : 0 + Layout.fillHeight: true + implicitWidth: 1 + color: ColorUtils.transparentize(Looks.colors.fg, 0.75) + } + WButton { + visible: !root.checked + Layout.fillHeight: true + implicitWidth: 47 + topLeftRadius: 0 + bottomLeftRadius: 0 + onClicked: root.requestFocus() + contentItem: Item { + FluentIcon { + anchors.centerIn: parent + icon: "chevron-right" + implicitSize: 14 + } + } + } + } + + component EntryNameColumn: ColumnLayout { + spacing: 4 + + WText { + Layout.fillWidth: true + wrapMode: Text.Wrap + text: root.entry.name + font.pixelSize: Looks.font.pixelSize.large + maximumLineCount: 2 + elide: Text.ElideRight + } + + WText { + Layout.fillWidth: true + visible: root.firstEntry + text: root.entry.type + color: Looks.colors.accentUnfocused + elide: Text.ElideRight + } + } + + MouseArea { + anchors.fill: parent + // hoverEnabled: true + acceptedButtons: Qt.NoButton + cursorShape: Qt.PointingHandCursor + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchResults.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchResults.qml new file mode 100644 index 000000000..fc4adc684 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchResults.qml @@ -0,0 +1,269 @@ +pragma ComponentBehavior: Bound +import qs +import qs.services +import qs.modules.common +import qs.modules.waffle.looks +import qs.modules.common.functions +import qs.modules.common.models +import qs.modules.waffle.startMenu +import Quickshell +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick + +RowLayout { + id: root + + property int maxResultsPerCategory: 4 + property int resultLimit: 20 + property StartMenuContext context + property int currentIndex: context.currentIndex + onCurrentIndexChanged: { + forceCurrentIndex(currentIndex); + } + function focusFirstItem() { + forceCurrentIndex(0); + } + function forceCurrentIndex(index) { + context.currentIndex = index; + // Somehow this hack is needed + if (index === 0) { + resultList.incrementCurrentIndex(); + resultList.decrementCurrentIndex(); + } else { + resultList.decrementCurrentIndex(); + resultList.incrementCurrentIndex(); + } + } + + Connections { + target: context + function onAccepted() { + resultList.currentItem?.execute(); + } + } + + ResultList { + id: resultList + Layout.fillHeight: true + Layout.fillWidth: true + } + ResultPreview { + Layout.preferredWidth: 386 + Layout.leftMargin: 1 + Layout.rightMargin: 1 + entry: resultList.model[resultList.currentIndex] ?? searchResultComp.createObject() + } + + component ResultList: WListView { + id: resultListView + section { + criteria: ViewSection.FullString + property: "category" // This is "type" with tweaks to make it match more closely + labelPositioning: ViewSection.InlineLabels + delegate: Item { + id: sectionButton + required property string section + implicitHeight: sectionChoiceButton.implicitHeight + resultListView.spacing + width: ListView.view?.width + WChoiceButton { + id: sectionChoiceButton + anchors { + left: parent.left + right: parent.right + top: parent.top + } + implicitHeight: 38 + contentItem: WText { + text: sectionButton.section + font.pixelSize: Looks.font.pixelSize.large + font.weight: Looks.font.weight.strong + } + onClicked: { + root.context.selectCategory(sectionButton.section); + } + } + } + } + clip: true + spacing: 4 + currentIndex: root.currentIndex + + // We can't use a ScriptModel here because it would mess up sections + model: { + const allResults = LauncherSearch.results; + // Find categories + var categories = new Set(); + for (let i = 0; i < allResults.length; i++) { + categories.add(allResults[i].type); + } + + // Collect max 4 per category + var categorizedResults = []; + let categoriesArray = Array.from(categories); + let totalCount = 0; + for (let c = 0; c < categoriesArray.length; c++) { + let category = categoriesArray[c]; + let count = 0; + for (let i = 0; i < allResults.length; i++) { + if (allResults[i].type === category) { + if (totalCount >= root.resultLimit) { + break; + } + const entry = allResults[i]; + const tweakedEntry = searchResultComp.createObject(null, Object.assign({}, entry)); + tweakedEntry.category = categorizedResults.length === 0 ? Translation.tr("Best match") : entry.type; + + categorizedResults.push(tweakedEntry); // Section header + count++; + totalCount++; + if (count >= root.maxResultsPerCategory) { + break; + } + } + } + if (totalCount >= root.resultLimit) { + break; + } + } + + // print(JSON.stringify(categorizedResults, null, 2)); + return categorizedResults; + } + onModelChanged: { + root.focusFirstItem(); + } + delegate: SearchResultButton { + required property int index + required property var modelData + entry: modelData + firstEntry: index === 0 + width: ListView.view?.width + checked: resultListView.currentIndex === index + onRequestFocus: { + root.forceCurrentIndex(index); + } + } + } + + component ResultPreview: Rectangle { + id: resultPreview + + property LauncherSearchResult entry // LauncherSearchResult + + Layout.fillHeight: true + color: Looks.colors.bg1 + radius: Looks.radius.large + + ColumnLayout { + anchors.fill: parent + anchors.margins: 22 + spacing: 13 + + ColumnLayout { + id: mainInfoColumn + Layout.alignment: Qt.AlignHCenter + SearchEntryIcon { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 10 + Layout.bottomMargin: 12 + entry: resultPreview.entry + iconSize: 64 + } + WText { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 2 + text: resultPreview.entry?.name || "" + font.pixelSize: Looks.font.pixelSize.xlarger + } + WText { + Layout.alignment: Qt.AlignHCenter + text: resultPreview.entry?.type || "" + color: Looks.colors.accentUnfocused + font.pixelSize: Looks.font.pixelSize.normal + } + } + Rectangle { + id: resultSeparator + implicitHeight: 2 + Layout.topMargin: 16 + Layout.fillWidth: true + color: Looks.colors.bg2Hover + } + WListView { + id: actionsColumn + Layout.fillHeight: true + Layout.fillWidth: true + clip: true + spacing: 2 + model: { + const isAppEntry = resultPreview.entry.type === Translation.tr("App"); + const appId = isAppEntry ? resultPreview.entry.id : ""; + const pinned = isAppEntry ? (Config.options.dock.pinnedApps.includes(appId)) : false; + const startPinned = isAppEntry ? (Config.options.launcher.pinnedApps.includes(appId)) : false; + var result = [ + searchResultComp.createObject(null, { + name: resultPreview.entry.verb, + iconName: isAppEntry ? "open_in_new" : "keyboard_return", + iconType: LauncherSearchResult.IconType.Material, + execute: () => { + resultPreview.entry.execute(); + } + }), + ...(isAppEntry ? [ + searchResultComp.createObject(null, { + name: startPinned ? Translation.tr("Unpin from Start") : Translation.tr("Pin to Start"), + iconName: startPinned ? "keep_off" : "keep", + iconType: LauncherSearchResult.IconType.Material, + execute: () => { + LauncherApps.togglePin(appId); + } + }) + ] : []), + ...(isAppEntry ? [ + searchResultComp.createObject(null, { + name: pinned ? Translation.tr("Unpin from taskbar") : Translation.tr("Pin to taskbar"), + iconName: pinned ? "keep_off" : "keep", + iconType: LauncherSearchResult.IconType.Material, + execute: () => { + TaskbarApps.togglePin(appId); + } + }) + ] : []), + ]; + result = result.concat(resultPreview.entry.actions); + return result; + } + delegate: WButton { + id: actionButton + required property var modelData + width: ListView.view?.width + icon.name: modelData.iconName + text: modelData.name + onClicked: modelData.execute(); + + contentItem: RowLayout { + spacing: 11 + SearchEntryIcon { + entry: actionButton.modelData + iconSize: 16 + } + WText { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + text: actionButton.text + } + } + } + } + } + } + + Component { + id: searchResultComp + LauncherSearchResult {} + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/TagStrip.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/TagStrip.qml new file mode 100644 index 000000000..33aef1906 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/TagStrip.qml @@ -0,0 +1,82 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks +import qs.modules.waffle.startMenu + +RowLayout { + id: root + property StartMenuContext context + + WPanelIconButton { + implicitWidth: 36 + implicitHeight: 36 + iconSize: 24 + iconName: "arrow-left" + onClicked: LauncherSearch.query = "" + } + ListView { + id: tagListView + Layout.fillWidth: true + Layout.fillHeight: true + orientation: Qt.Horizontal + spacing: 4 + model: root.context.categories + clip: true + delegate: WBorderedButton { + id: tagButton + required property var modelData + border.width: 1 + radius: height / 2 + implicitWidth: tagButtonText.implicitWidth + 12 * 2 + implicitHeight: 32 + checked: { + if (modelData.prefix != "") { + return LauncherSearch.query.startsWith(modelData.prefix); + } else { + return !tagListView.model.some(i => (i.prefix != "" && LauncherSearch.query.startsWith(i.prefix))); + } + } + contentItem: Item { + WText { + id: tagButtonText + anchors.centerIn: parent + color: tagButton.fgColor + text: tagButton.modelData.name + font.pixelSize: Looks.font.pixelSize.large + } + } + onClicked: LauncherSearch.ensurePrefix(tagButton.modelData.prefix) + } + } + WPanelIconButton { + id: optionsButton + implicitWidth: 36 + implicitHeight: 36 + iconSize: 24 + iconName: "more-horizontal" + + onClicked: accountsMenu.open() + + WMenu { + id: accountsMenu + x: -accountsMenu.implicitWidth + optionsButton.implicitWidth + 10 + y: optionsButton.height + downDirection: true + Action { + icon.name: "people-settings" + text: Translation.tr("Manage accounts") + onTriggered: { + Quickshell.execDetached(["bash", "-c", Config.options.apps.manageUser]) + GlobalStates.searchOpen = false; + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/AggregatedAppCategoryModel.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/AggregatedAppCategoryModel.qml new file mode 100644 index 000000000..e3886235c --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/AggregatedAppCategoryModel.qml @@ -0,0 +1,7 @@ +import QtQuick +import qs.services + +QtObject { + property string name + property list categories +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/AllAppsGrid.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/AllAppsGrid.qml new file mode 100644 index 000000000..4a704909d --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/AllAppsGrid.qml @@ -0,0 +1,84 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +GridLayout { + id: root + + columns: 4 + + Component { + id: aggAppCatComp + AggregatedAppCategoryModel {} + } + property list aggregatedCategories: [ + aggAppCatComp.createObject(null, { + name: Translation.tr("Productivity"), + categories: ["Development", "Education", "Network", "Office"] + }), aggAppCatComp.createObject(null, { + name: Translation.tr("Utilities & Tools"), + categories: ["Utility", "Science"] + }), aggAppCatComp.createObject(null, { + name: Translation.tr("Creativity"), + categories: ["AudioVideo", "Graphics"] + }), aggAppCatComp.createObject(null, { + name: Translation.tr("System"), + categories: ["Settings", "System"] + }), aggAppCatComp.createObject(null, { + name: Translation.tr("Other"), + categories: ["Game"] + }), + ] + + Repeater { + model: root.aggregatedCategories + delegate: AppCategory { + required property var modelData + aggregatedCategory: modelData + } + } + + columnSpacing: 27 + rowSpacing: 12 + component AppCategory: Item { + id: categoryItem + property AggregatedAppCategoryModel aggregatedCategory + implicitWidth: categoryLayout.implicitWidth + implicitHeight: categoryLayout.implicitHeight + ColumnLayout { + id: categoryLayout + anchors.fill: parent + spacing: 4 + + AppCategoryGrid { + id: categoryGrid + Layout.fillWidth: true + aggregatedCategory: categoryItem.aggregatedCategory + } + + WButton { + id: categoryButton + Layout.fillWidth: true + implicitHeight: 32 + + contentItem: WText { + id: categoryButtonText + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + text: categoryItem.aggregatedCategory.name + } + onClicked: { + categoryGrid.openCategoryFolder(); + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/AppCategoryGrid.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/AppCategoryGrid.qml new file mode 100644 index 000000000..fa38e53f3 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/AppCategoryGrid.qml @@ -0,0 +1,341 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +Rectangle { + id: root + property AggregatedAppCategoryModel aggregatedCategory + property list desktopEntries: [...DesktopEntries.applications.values.filter(app => { + const appCategories = app.categories; + const gridCategories = root.aggregatedCategory.categories; + return appCategories.some(cat => gridCategories.indexOf(cat) !== -1); + })].sort((a, b) => a.name.localeCompare(b.name)); + + property Item windowRootItem: { + var item = root; + // print("FINDING ROOT") + while (item.parent != null) { + if (item.parent.toString().includes("ProxyWindow")) + break; + item = item.parent; + } + // print(item.width, item.height) + return item; + } + function openCategoryFolder() { + categoryFolderPopup.open(); + } + + radius: Looks.radius.large + color: Looks.colors.bg1 + border.width: 1 + border.color: ColorUtils.transparentize(Looks.colors.ambientShadow, 0.7) + implicitWidth: 156 + implicitHeight: 156 + + GridLayout { + id: categoryAppsGrid + anchors.fill: parent + anchors.margins: 10 + columns: 2 + rows: 2 + columnSpacing: 0 + rowSpacing: 0 + uniformCellHeights: true + uniformCellWidths: true + + Repeater { + model: ScriptModel { + values: root.desktopEntries.slice(0, 3) + } + delegate: SmallGridAppButton { + required property DesktopEntry modelData + desktopEntry: modelData + } + } + Loader { + id: categoryOpenButtonLoader + // It's like this on the real thing - you get an invisible button if there's not enough items + opacity: root.desktopEntries.length > 3 ? 1 : 0 + active: true + sourceComponent: CategoryOpenButton { + aggregatedCategory: root.aggregatedCategory + } + } + } + + Popup { + id: categoryFolderPopup + // I don't even know what the fuck is going on at this point + // I hate point mapping + property point originPoint: categoryOpenButtonLoader.mapToItem(root, categoryOpenButtonLoader.width / 2, categoryOpenButtonLoader.height / 2) + property point windowCenterPoint: { + const rootContentItem = root.windowRootItem; + const canvasPosInRoot = root.mapFromItem(rootContentItem, rootContentItem.width / 2, rootContentItem.height / 2); + const sectionItem = root.parent.parent.parent; + const positionInSection = sectionItem.mapFromItem(categoryOpenButtonLoader, categoryOpenButtonLoader.x, categoryOpenButtonLoader.y); + const targetY = Math.max(-positionInSection.y + 212, canvasPosInRoot.y); + return Qt.point(canvasPosInRoot.x, targetY); + } + + enter: Transition { + NumberAnimation { + target: categoryFolderPopup + property: "x" + from: categoryFolderPopup.originPoint.x - categoryOpenButtonLoader.width * 5 / 2 + to: categoryFolderPopup.windowCenterPoint.x - categoryFolderPopup.width / 2 + duration: 300 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + NumberAnimation { + target: categoryFolderPopup + property: "y" + from: categoryFolderPopup.originPoint.y - categoryOpenButtonLoader.height * 3 / 2 + to: categoryFolderPopup.windowCenterPoint.y - categoryFolderPopup.height / 2 + duration: 300 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + NumberAnimation { + target: categoryFolderPopup + property: "scale" + from: 0 + to: 1 + duration: 300 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + } + + exit: Transition { + NumberAnimation { + target: categoryFolderPopup + property: "x" + to: categoryFolderPopup.originPoint.x - categoryOpenButtonLoader.width * 5 / 2 + duration: 200 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeOut + } + NumberAnimation { + target: categoryFolderPopup + property: "y" + to: categoryFolderPopup.originPoint.y - categoryOpenButtonLoader.height * 3 / 2 + duration: 200 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeOut + } + NumberAnimation { + target: categoryFolderPopup + property: "scale" + from: 1 + to: 0 + duration: 200 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeOut + } + } + + background: null + + Loader { + id: folderContentLoader + active: categoryFolderPopup.visible + sourceComponent: WRectangularShadowThis { + CategoryFolderContent { + title: root.aggregatedCategory.name + desktopEntries: root.desktopEntries + } + } + } + } + + component CategoryFolderContent: WToolTipContent { + id: categoryFolderContent + property string title + property list desktopEntries: root.desktopEntries + horizontalPadding: 0 + verticalPadding: 0 + radius: Looks.radius.large + realContentItem: Item { + implicitWidth: 448 + implicitHeight: 376 + ColumnLayout { + anchors { + fill: parent + leftMargin: 32 + rightMargin: 32 + topMargin: 40 + bottomMargin: 32 + } + spacing: 28 + WText { + Layout.fillWidth: true + text: categoryFolderContent.title + font.pixelSize: Looks.font.pixelSize.xlarger + font.weight: Looks.font.weight.stronger + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + SwipeView { + id: categoryFolderSwipeView + anchors.fill: parent + orientation: Qt.Vertical + clip: true + + Repeater { + model: Math.ceil(root.desktopEntries.length / 12) + delegate: Item { + id: folderPage + required property int index + width: SwipeView.view.width + height: SwipeView.view.height + BigAppGrid { + anchors { + top: parent.top + left: parent.left + } + columns: 4 + rows: 3 + desktopEntries: root.desktopEntries.slice(folderPage.index * 12, (folderPage.index + 1) * 12) + } + } + } + } + VerticalPageIndicator { + anchors.verticalCenter: parent.verticalCenter + anchors.right: categoryFolderSwipeView.right + anchors.rightMargin: -19 + + showArrows: false + currentIndex: categoryFolderSwipeView.currentIndex + count: Math.ceil(root.desktopEntries.length / 12) + onClicked: index => categoryFolderSwipeView.currentIndex = index + } + } + } + FocusedScrollMouseArea { + z: 999 + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: false + onScrollUp: categoryFolderSwipeView.decrementCurrentIndex() + onScrollDown: categoryFolderSwipeView.incrementCurrentIndex() + } + } + } + + component CategoryOpenButton: SmallGridButton { + id: categoryOpenButton + property AggregatedAppCategoryModel aggregatedCategory + + onClicked: root.openCategoryFolder() + contentItem: Item { + Behavior on scale { + NumberAnimation { + id: scaleAnim + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + } + GridLayout { + anchors.centerIn: parent + rows: 2 + columns: 2 + rowSpacing: 2 + columnSpacing: 2 + + Repeater { + model: root.desktopEntries.slice(3, 7) + delegate: WAppIcon { + required property DesktopEntry modelData + tryCustomIcon: false + iconName: modelData.icon + implicitSize: 16 + } + } + } + } + } + + component SmallGridAppButton: SmallGridButton { + id: smallGridAppButton + property DesktopEntry desktopEntry + + property bool pinnedStart: LauncherApps.isPinned(smallGridAppButton.desktopEntry.id); + property bool pinnedTaskbar: TaskbarApps.isPinned(smallGridAppButton.desktopEntry.id); + + onClicked: { + GlobalStates.searchOpen = false; + desktopEntry.execute(); + } + + contentItem: Item { + Behavior on scale { + NumberAnimation { + id: scaleAnim + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + } + WAppIcon { + anchors.centerIn: parent + tryCustomIcon: false + iconName: smallGridAppButton.desktopEntry.icon + implicitSize: 34 + } + } + + WToolTip { + text: smallGridAppButton.desktopEntry.name + } + + altAction: () => { + appMenu.popup(); + } + + WMenu { + id: appMenu + downDirection: true + + WMenuItem { + icon.name: smallGridAppButton.pinnedStart ? "pin-off" : "pin" + text: smallGridAppButton.pinnedStart ? Translation.tr("Unpin from Start") : Translation.tr("Pin to Start") + onTriggered: { + LauncherApps.togglePin(smallGridAppButton.desktopEntry.id); + } + } + WMenuItem { + icon.name: smallGridAppButton.pinnedTaskbar ? "pin-off" : "pin" + text: smallGridAppButton.pinnedTaskbar ? Translation.tr("Unpin from taskbar") : Translation.tr("Pin to taskbar") + onTriggered: { + TaskbarApps.togglePin(smallGridAppButton.desktopEntry.id); + } + } + } + } + + component SmallGridButton: WButton { + id: root + implicitWidth: 68 + implicitHeight: 68 + + property real pressedScale: 5 / 6 + + onDownChanged: { + contentItem.scale = root.down ? root.pressedScale : 1; // If/When we do dragging, the scale is 1.25 + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/BigAppGrid.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/BigAppGrid.qml new file mode 100644 index 000000000..dbc44adf8 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/BigAppGrid.qml @@ -0,0 +1,37 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +GridLayout { + id: root + + property list desktopEntries: [] + + columnSpacing: 0 + rowSpacing: 0 + + uniformCellHeights: true + uniformCellWidths: true + + Repeater { + model: root.desktopEntries + delegate: StartAppButton { + id: pinnedAppButton + required property var modelData + desktopEntry: modelData + onClicked: { + GlobalStates.searchOpen = false; + desktopEntry.execute(); + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartAppButton.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartAppButton.qml new file mode 100644 index 000000000..8df30bd85 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartAppButton.qml @@ -0,0 +1,98 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +WButton { + id: root + required property DesktopEntry desktopEntry + + property bool pinnedStart: LauncherApps.isPinned(root.desktopEntry.id); + property bool pinnedTaskbar: TaskbarApps.isPinned(root.desktopEntry.id); + + implicitWidth: 96 + implicitHeight: 84 + horizontalPadding: 0 + verticalPadding: 0 + contentItem: ColumnLayout { + spacing: 3 + WAppIcon { + Layout.topMargin: 12 + Layout.alignment: Qt.AlignHCenter + iconName: root.desktopEntry.icon + implicitSize: 34 + tryCustomIcon: false + } + WText { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + text: root.desktopEntry.name + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + } + } + WToolTip { + text: root.desktopEntry.name + } + + altAction: () => { + appMenu.popup() + } + + WMenu { + id: appMenu + downDirection: true + + WMenuItem { + visible: root.pinnedStart + icon.name: "arrow-up-left" + text: Translation.tr("Move to front") + onTriggered: { + LauncherApps.moveToFront(root.desktopEntry.id); + } + } + WMenuItem { + visible: root.pinnedStart + icon.name: "arrow-left" + text: Translation.tr("Move left") + onTriggered: { + LauncherApps.moveLeft(root.desktopEntry.id); + } + } + WMenuItem { + visible: root.pinnedStart + icon.name: "arrow-right" + text: Translation.tr("Move right") + onTriggered: { + LauncherApps.moveRight(root.desktopEntry.id); + } + } + WMenuItem { + icon.name: root.pinnedStart ? "pin-off" : "pin" + text: root.pinnedStart ? Translation.tr("Unpin from Start") : Translation.tr("Pin to Start") + onTriggered: { + LauncherApps.togglePin(root.desktopEntry.id); + } + } + WMenuItem { + icon.name: root.pinnedTaskbar ? "pin-off" : "pin" + text: root.pinnedTaskbar ? Translation.tr("Unpin from taskbar") : Translation.tr("Pin to taskbar") + onTriggered: { + TaskbarApps.togglePin(root.desktopEntry.id); + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartPageApps.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartPageApps.qml new file mode 100644 index 000000000..b4539b317 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartPageApps.qml @@ -0,0 +1,76 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +BodyRectangle { + id: root + + ColumnLayout { + anchors { + fill: parent + leftMargin: 32 + rightMargin: 32 + topMargin: 25 + bottomMargin: 30 + } + spacing: 26 + + PinnedApps { + Layout.fillWidth: true + } + + AllApps { + implicitHeight: 300 // for now + } + } + + component PinnedApps: PageSection { + title: Translation.tr("Pinned") + + BigAppGrid { + Layout.fillWidth: true + columns: 8 + desktopEntries: Config.options.launcher.pinnedApps.map(appId => DesktopEntries.byId(appId)) + } + } + + component AllApps: PageSection { + title: Translation.tr("All") + // TODO: Do we wanna also implement list view and grid view? + // (instead of only category view) + AllAppsGrid { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 32 + Layout.rightMargin: 32 + } + } + + component PageSection: ColumnLayout { + id: pageSection + required property string title + default property alias data: pageSectionContentArea.data + + spacing: 16 + + WText { + Layout.leftMargin: 32 + text: pageSection.title + font.pixelSize: Looks.font.pixelSize.large + font.weight: Looks.font.weight.stronger + } + + ColumnLayout { + id: pageSectionContentArea + Layout.fillWidth: true + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartPageContent.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartPageContent.qml new file mode 100644 index 000000000..3d0eabb8a --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartPageContent.qml @@ -0,0 +1,99 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +WPanelPageColumn { + id: root + + WPanelSeparator {} + + StartPageApps { + Layout.fillHeight: true + } + + WPanelSeparator {} + + StartFooter { + Layout.fillWidth: true + } + + component StartFooter: FooterRectangle { + implicitHeight: 63 + + StartUserButton { + anchors { + left: parent.left + leftMargin: 52 + bottom: parent.bottom + bottomMargin: 12 + } + } + + PowerButton { + anchors { + right: parent.right + rightMargin: 52 + bottom: parent.bottom + bottomMargin: 12 + } + } + } + + component PowerButton: WBorderlessButton { + id: powerButton + implicitWidth: 40 + implicitHeight: 40 + + contentItem: Item { + FluentIcon { + anchors.centerIn: parent + icon: "power" + implicitSize: 20 + } + } + + WToolTip { + extraVisibleCondition: !powerMenu.visible + text: qsTr("Power") + } + + onClicked: { + powerMenu.open() + } + + WMenu { + id: powerMenu + x: -powerMenu.implicitWidth / 2 + powerButton.implicitWidth / 2 + y: -powerMenu.implicitHeight - 4 + Action { + icon.name: "lock-closed" + text: Translation.tr("Lock") + onTriggered: Session.lock() + } + Action { + icon.name: "weather-moon" + text: Translation.tr("Sleep") + onTriggered: Session.suspend() + } + Action { + icon.name: "power" + text: Translation.tr("Shut down") + onTriggered: Session.poweroff() + } + Action { + icon.name: "arrow-counterclockwise" + text: Translation.tr("Restart") + onTriggered: Session.reboot() + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartUserButton.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartUserButton.qml new file mode 100644 index 000000000..274883c72 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/startPage/StartUserButton.qml @@ -0,0 +1,140 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +WBorderlessButton { + id: userButton + implicitWidth: userButtonRow.implicitWidth + 12 * 2 + implicitHeight: 40 + + contentItem: Item { + RowLayout { + id: userButtonRow + anchors.centerIn: parent + spacing: 12 + + WUserAvatar { + sourceSize: Qt.size(32, 32) + } + WText { + Layout.alignment: Qt.AlignVCenter + text: SystemInfo.username + } + } + } + + onClicked: { + userMenu.open(); + } + + WToolTip { + text: SystemInfo.username + } + + Popup { + id: userMenu + x: -51 + y: -userMenu.implicitHeight + userButton.implicitHeight / 2 - 10 + + background: null + + WToolTipContent { + id: popupContent + horizontalPadding: 10 + verticalPadding: 7 + radius: Looks.radius.large + realContentItem: Item { + implicitWidth: userMenuContentLayout.implicitWidth + implicitHeight: userMenuContentLayout.implicitHeight + + ColumnLayout { + id: userMenuContentLayout + anchors { + fill: parent + leftMargin: popupContent.horizontalPadding + rightMargin: popupContent.horizontalPadding + topMargin: popupContent.verticalPadding + bottomMargin: popupContent.verticalPadding + } + spacing: 5 + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 6 + FluentIcon { + Layout.alignment: Qt.AlignVCenter + implicitSize: 22 + icon: "corporation" + monochrome: false + } + WText { + Layout.alignment: Qt.AlignVCenter + text: "Megahard" + font.pixelSize: Looks.font.pixelSize.large + font.weight: Looks.font.weight.strong + } + Item { Layout.fillWidth: true } + WBorderlessButton { + Layout.alignment: Qt.AlignVCenter + implicitHeight: 36 + implicitWidth: textItem.implicitWidth + 10 * 2 + contentItem: WText { + id: textItem + text: Translation.tr("Sign out") + font.pixelSize: Looks.font.pixelSize.large + } + onClicked: Session.logout() + } + } + Item { // Force min width 360 (using min on the item somehow doesn't work) + implicitWidth: 334 + } + RowLayout { + Layout.fillWidth: true + Layout.bottomMargin: 7 + Layout.leftMargin: 6 + spacing: 12 + WUserAvatar { + sourceSize: Qt.size(58, 58) + } + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 2 + WText { + text: SystemInfo.username + font.pixelSize: Looks.font.pixelSize.larger + font.weight: Looks.font.weight.strong + } + WText { + color: Looks.colors.fg1 + text: Translation.tr("Local account") + } + WText { + color: Looks.colors.accent + text: Translation.tr("Manage my account") + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Quickshell.execDetached(["bash", "-c", Config.options.apps.manageUser]) + GlobalStates.searchOpen = false; + } + } + } + } + } + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewContent.qml b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewContent.qml new file mode 100644 index 000000000..6828098cf --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewContent.qml @@ -0,0 +1,301 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.models +import qs.modules.common.widgets +import qs.modules.waffle.looks +import "window-layout.js" as WindowLayout + +Rectangle { + id: root + + color: ColorUtils.transparentize(Looks.colors.bg1Base, 0.5) + property bool draggingWindow: false + property real openProgress: 0 + property Item hoveredWorkspace: null + signal closed + + Component.onCompleted: { + openAnim.start(); + } + function close() { + closeAnim.start(); + } + + PropertyAnimation { + id: openAnim + target: root + property: "openProgress" + to: 1 + duration: 250 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + SequentialAnimation { + id: closeAnim + + PropertyAnimation { + target: root + property: "openProgress" + to: 0 + duration: 250 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + ScriptAction { + script: { + root.closed(); + } + } + } + + // Windows + property real maxWindowHeight: 290 + property real maxWindowWidth: 738 + property real padding: 52 + property real spacing: 25 + readonly property list toplevels: ToplevelManager.toplevels.values.filter(t => { + const client = HyprlandData.clientForToplevel(t); + return client && client.workspace.id === HyprlandData.activeWorkspace?.id; + }) + readonly property list arrangedToplevels: { + const maxRowWidth = width - padding * 2; + const count = toplevels.length; + const resultLayout = []; + + var i = 0; + while (i < count) { + var row = []; + var rowWidth = 0; + var j = i; + + while (j < count) { + const toplevel = toplevels[j]; + const client = HyprlandData.clientForToplevel(toplevel); + const scaledSize = WindowLayout.scaleWindow(client, maxWindowWidth, maxWindowHeight); + + if (rowWidth + scaledSize.width <= maxRowWidth || row.length === 0) { + row.push(toplevel); + rowWidth += scaledSize.width; + j++; + } else { + break; + } + } + + resultLayout.push(row); + i = j; + } + return resultLayout; + } + + MouseArea { + z: 0 + anchors.fill: parent + onClicked: { + GlobalStates.overviewOpen = false; + } + } + + // Windows + WListView { + id: windowListView + z: root.openProgress == 1 ? 2 : 1 + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: (root.height - (wsBorder.height + 16) - height) / 2 + } + spacing: root.spacing + topMargin: root.padding + bottomMargin: root.padding + leftMargin: root.padding + rightMargin: root.padding + height: Math.min(contentHeight + topMargin + bottomMargin, root.height - (wsBorder.height + 16)) + + interactive: (height < contentHeight) && !root.draggingWindow + clip: root.openProgress > 0.99 && !root.draggingWindow + + model: ScriptModel { + values: root.arrangedToplevels + } + delegate: RowLayout { + id: clientRow + required property var modelData + spacing: root.spacing + anchors.horizontalCenter: parent?.horizontalCenter ?? undefined + + Repeater { + model: ScriptModel { + values: clientRow.modelData + } + delegate: Item { + id: clientGridArea + required property int index + required property var modelData + implicitWidth: windowItem.openedSize.width + implicitHeight: windowItem.openedSize.height + windowItem.titleBarImplicitHeight + + TaskViewWindow { + id: windowItem + z: Drag.active ? 2 : 1 + opacity: openAnim.running ? root.openProgress : 1 + + property int mappedX: { + // print("AAAWAWAAWAWWA: ", -(clientRow.x + clientGridArea.x + root.padding)); + var rootPosToThis = -(clientRow.x + clientGridArea.x + root.padding); + return rootPosToThis + hyprlandClient.at[0]; + } + property int mappedY: { + // print("AAAWAWAAWAWWA YYYY YUIUSDFOIU: ", clientRow.y + windowListView.y + root.padding + windowItem.titleBarImplicitHeight) + var rootPosToThis = -(clientRow.y + windowListView.y + root.padding + windowItem.titleBarImplicitHeight); + return rootPosToThis + hyprlandClient.at[1]; + } + property int openedX: 0 + property int openedY: 0 + // property int openedX: Drag.active ? (dragHandler.xAxis.activeValue) : 0 + // property int openedY: Drag.active ? (dragHandler.yAxis.activeValue) : 0 + scaleSize: (root.openProgress > 0 && !closeAnim.running) + x: mappedX + (openedX - mappedX) * root.openProgress + y: mappedY + (openedY - mappedY) * root.openProgress + + droppable: root.hoveredWorkspace !== null + Drag.active: dragHandler.active + Drag.hotSpot.x: mouseX + Drag.hotSpot.y: mouseY + + DragHandler { + id: dragHandler + target: null + xAxis.onActiveValueChanged: { + windowItem.openedX = dragHandler.xAxis.activeValue; + } + yAxis.onActiveValueChanged: { + windowItem.openedY = dragHandler.yAxis.activeValue; + } + onActiveChanged: { + if (active) { + root.draggingWindow = true; + } else { + root.draggingWindow = false; + if (root.hoveredWorkspace !== null && root.hoveredWorkspace.workspace !== windowItem.hyprlandClient.workspace.id) { + Hyprland.dispatch(`movetoworkspacesilent ${root.hoveredWorkspace.workspace}, address:${windowItem.hyprlandClient.address}`); + } else { + windowItem.openedX = 0; + windowItem.openedY = 0; + } + } + } + } + + Layout.alignment: Qt.AlignTop + maxHeight: root.maxWindowHeight + maxWidth: root.maxWindowWidth + toplevel: clientGridArea.modelData + } + } + } + } + } + + // Workspaces + Rectangle { + id: wsBorder + z: root.openProgress == 1 ? 1 : 2 + property real sourceEdgeMargin: -(height + 8) + root.openProgress * (height + 16) + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + leftMargin: 8 + rightMargin: 8 + topMargin: sourceEdgeMargin + bottomMargin: sourceEdgeMargin + } + border.color: Looks.colors.bg2Border + border.width: 1 + radius: Looks.radius.large + color: "transparent" + + implicitHeight: wsBg.implicitHeight + border.width * 2 + + Rectangle { + id: wsBg + anchors.fill: parent + anchors.margins: wsBorder.border.width + radius: wsBorder.radius - wsBorder.border.width + color: Looks.colors.bgPanelFooterBackground + + implicitHeight: 174 + + WListView { + id: workspaceListView + anchors { + top: parent.top + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + topMargin: 5 + bottomMargin: 5 + } + flickableDirection: Flickable.HorizontalFlick + orientation: ListView.Horizontal + interactive: width == parent.width + width: Math.min(contentWidth + leftMargin + rightMargin, parent.width) + leftMargin: 5 + rightMargin: 5 + clip: true + spacing: 4 + + function reposition() { + positionViewAtIndex(HyprlandData.activeWorkspace.id - 1, ListView.Contain); + } + + Connections { + target: HyprlandData + function onActiveWorkspaceChanged() { + workspaceListView.reposition(); + } + } + model: IndexModel { + id: workspaceIndexModel + count: { + const maxWorkspaceId = Math.max.apply(null, HyprlandData.workspaces.map(ws => ws.id)); + return Math.max(maxWorkspaceId, 1) + 1; + } + } + delegate: TaskViewWorkspace { + id: workspaceItem + required property int index + workspace: index + 1 + newWorkspace: index == workspaceIndexModel.count - 1 + + droppable: root.hoveredWorkspace === workspaceItem + DropArea { + anchors.fill: parent + onEntered: drag => { + root.hoveredWorkspace = workspaceItem; + } + onExited: { + if (root.hoveredWorkspace === workspaceItem) { + root.hoveredWorkspace = null; + } + } + } + + onClicked: { + GlobalStates.overviewOpen = false; + root.closed(); // Close immediately to avoid weird animations + Hyprland.dispatch(`workspace ${workspaceItem.workspace}`); + } + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWindow.qml b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWindow.qml new file mode 100644 index 000000000..b91655097 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWindow.qml @@ -0,0 +1,155 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks +import "window-layout.js" as WindowLayout + +WMouseAreaButton { + id: root + + required property var toplevel + required property int maxHeight + required property int maxWidth + + property var hyprlandClient: HyprlandData.clientForToplevel(root.toplevel) + property string address: hyprlandClient?.address + + property string iconName: AppSearch.guessIcon(hyprlandClient?.class) + + color: drag.active ? ColorUtils.transparentize(Looks.colors.bg1Base) : (containsMouse ? Looks.colors.bg1Base : Looks.colors.bgPanelFooterBackground) + borderColor: ColorUtils.transparentize(Looks.colors.bg2Border, drag.active ? 1 : 0) + radius: Looks.radius.xLarge + + property real titleBarImplicitHeight: titleBar.implicitHeight + property bool scaleSize: true + property size openedSize: WindowLayout.scaleWindow(hyprlandClient, maxWidth, maxHeight); + property size fullSize: Qt.size(hyprlandClient?.size[0] ?? maxWidth, hyprlandClient?.size[1] ?? maxHeight) + property size size: scaleSize ? openedSize : fullSize + implicitWidth: Math.max(Math.round(contentItem.implicitWidth), 138) + implicitHeight: Math.round(contentItem.implicitHeight) + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: root.background.width + height: root.background.height + Rectangle { + radius: root.background.radius + anchors { + fill: parent + topMargin: root.drag.active ? root.titleBarImplicitHeight : 0 + } + } + } + } + property bool droppable: false + scale: (root.pressedButtons & Qt.LeftButton || root.Drag.active) ? (droppable ? 0.4 : 0.95) : 1 + Behavior on scale { + NumberAnimation { + id: scaleAnim + duration: 200 + easing.type: Easing.OutExpo + } + } + + function closeWindow() { + Hyprland.dispatch(`closewindow address:${root.hyprlandClient?.address}`); + } + + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton + onClicked: event => { + if (event.button === Qt.LeftButton) { + GlobalStates.overviewOpen = false; + Hyprland.dispatch(`focuswindow address:${root.hyprlandClient?.address}`); + GlobalStates.overviewOpen = false; + } else if (event.button === Qt.MiddleButton) { + root.closeWindow(); + event.accepted = true; + } else if (event.button === Qt.RightButton) { + if (!windowMenu.visible) + windowMenu.popup(); + else + windowMenu.close(); + } + } + + ColumnLayout { + id: contentItem + z: 2 + anchors.fill: parent + anchors.margins: 1 + spacing: 0 + + RowLayout { + id: titleBar + opacity: root.drag.active ? 0 : 1 + spacing: 8 + WAppIcon { + Layout.leftMargin: 10 + Layout.alignment: Qt.AlignVCenter + iconName: root.iconName + implicitSize: 16 + tryCustomIcon: false + } + WText { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + elide: Text.ElideRight + text: root.hyprlandClient?.title ?? "" + } + CloseButton { + implicitWidth: 38 + implicitHeight: 38 + padding: 8 + onClicked: root.closeWindow() + } + } + + ScreencopyView { + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter + implicitWidth: Math.round(root.size.width) + implicitHeight: Math.round(root.size.height) + constraintSize: Qt.size(Math.round(root.size.width), Math.round(root.size.height)) + + Behavior on implicitWidth { + animation: Looks.transition.enter.createObject(this) + } + Behavior on implicitHeight { + animation: Looks.transition.enter.createObject(this) + } + + captureSource: root.toplevel ?? null + live: true + } + } + + WMenu { + id: windowMenu + downDirection: true + + Action { + enabled: root.hyprlandClient?.floating + property bool isPinned: root.hyprlandClient?.pinned + icon.name: isPinned ? "checkmark" : "empty" + text: Translation.tr("Show this window on all desktops") + onTriggered: { + Hyprland.dispatch(`pin address:${root.hyprlandClient?.address}`); + } + } + Action { + icon.name: "empty" + text: Translation.tr("Close") + onTriggered: root.closeWindow() + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWorkspace.qml b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWorkspace.qml new file mode 100644 index 000000000..8fe3f35bd --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWorkspace.qml @@ -0,0 +1,145 @@ +import QtQuick +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks + +WMouseAreaButton { + id: root + + required property int workspace + property bool newWorkspace: false + property bool droppable: false + + readonly property bool isActiveWorkspace: HyprlandData.activeWorkspace?.id === root.workspace + readonly property real screenWidth: QsWindow.window?.width ?? 0 + readonly property real screenHeight: QsWindow.window?.height ?? 0 + readonly property real screenAspectRatio: screenWidth / screenHeight + readonly property real windowScale: wallpaperHeight / screenHeight + + property real wallpaperHeight: 124 + + height: ListView.view?.height ?? 100 + implicitWidth: 244 // for now + + colBackground: ColorUtils.transparentize(Looks.colors.bg2, (isActiveWorkspace || droppable) ? 0 : 1) + Behavior on color { + animation: Looks.transition.color.createObject(this) + } + + scale: root.containsPress ? 0.95 : 1 + Behavior on scale { + NumberAnimation { + id: scaleAnim + duration: 300 + easing.type: Easing.OutExpo + } + } + + // Content + ColumnLayout { + id: contentItem + anchors { + fill: parent + leftMargin: 12 + rightMargin: 12 + topMargin: 9 + bottomMargin: 8 + } + spacing: 8 + + WText { + Layout.fillWidth: true + Layout.fillHeight: false + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + text: root.newWorkspace ? Translation.tr("New desktop") : Translation.tr("Desktop %1").arg(root.workspace) + } + + Rectangle { + id: wsBg + height: root.wallpaperHeight + Layout.fillHeight: true + Layout.fillWidth: true + color: Looks.colors.bg1 + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: wsBg.width + height: wsBg.height + radius: Looks.radius.medium + } + } + + // Workspace content + Loader { + anchors.fill: parent + active: !root.newWorkspace + sourceComponent: StyledImage { + cache: true + sourceSize: Qt.size(root.screenAspectRatio * root.wallpaperHeight, root.wallpaperHeight) + source: Config.options.background.wallpaperPath + fillMode: Image.PreserveAspectCrop + + Repeater { + model: ScriptModel { + values: HyprlandData.toplevelsForWorkspace(root.workspace) + } + delegate: ScreencopyView { + required property var modelData + readonly property var hyprlandWindowData: HyprlandData.windowByAddress[`0x${modelData.HyprlandToplevel?.address}`] + captureSource: modelData + live: true + width: hyprlandWindowData?.size[0] * root.windowScale + height: hyprlandWindowData?.size[1] * root.windowScale + x: hyprlandWindowData?.at[0] * root.windowScale + y: hyprlandWindowData?.at[1] * root.windowScale + } + } + } + } + + // New plus icon + Loader { + anchors.centerIn: parent + active: root.newWorkspace + sourceComponent: FluentIcon { + icon: "add" + } + } + + Rectangle { + z: 2 + visible: root.droppable && !root.newWorkspace + anchors.fill: parent + color: Looks.colors.accent + opacity: 0.2 + } + } + } + + // Active indicator + WFadeLoader { + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + } + shown: root.isActiveWorkspace + + sourceComponent: Rectangle { + id: activeIndicator + implicitWidth: 32 + implicitHeight: 3 + color: Looks.colors.accent + radius: height / 2 + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/WaffleTaskView.qml b/dots/.config/quickshell/ii/modules/waffle/taskView/WaffleTaskView.qml new file mode 100644 index 000000000..05a2ccf40 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/taskView/WaffleTaskView.qml @@ -0,0 +1,110 @@ +pragma ComponentBehavior: Bound +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import Qt.labs.synchronizer +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +Scope { + id: overviewScope + property bool dontAutoCancelSearch: false + + Variants { + id: overviewVariants + model: Quickshell.screens + + Loader { + id: panelLoader + required property var modelData + active: false + Connections { + target: GlobalStates + function onOverviewOpenChanged() { + if (GlobalStates.overviewOpen) + panelLoader.active = true; + } + } + sourceComponent: PanelWindow { + id: root + property string searchingText: "" + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) + screen: panelLoader.modelData + + WlrLayershell.namespace: "quickshell:wTaskView" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + color: "transparent" + + anchors { + top: true + bottom: true + left: true + right: true + } + + TaskViewContent { + id: taskViewContent + anchors.fill: parent + + Component.onCompleted: { + taskViewContent.forceActiveFocus(); + } + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.overviewOpen = false; + } + } + + Connections { + target: GlobalStates + function onOverviewOpenChanged() { + if (!GlobalStates.overviewOpen) + taskViewContent.close(); + } + } + onClosed: panelLoader.active = false + } + } + } + } + + IpcHandler { + target: "search" + + function toggle() { + GlobalStates.overviewOpen = !GlobalStates.overviewOpen; + } + function workspacesToggle() { + GlobalStates.overviewOpen = !GlobalStates.overviewOpen; + } + function close() { + GlobalStates.overviewOpen = false; + } + function open() { + GlobalStates.overviewOpen = true; + } + function toggleReleaseInterrupt() { + GlobalStates.superReleaseMightTrigger = false; + } + function clipboardToggle() { + overviewScope.toggleClipboard(); + } + } + + GlobalShortcut { + name: "overviewWorkspacesToggle" + description: "Toggles overview on press" + + onPressed: { + GlobalStates.overviewOpen = !GlobalStates.overviewOpen; + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/window-layout.js b/dots/.config/quickshell/ii/modules/waffle/taskView/window-layout.js new file mode 100644 index 000000000..d5548bdb5 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/taskView/window-layout.js @@ -0,0 +1,36 @@ +function scaleWindow(hyprlandClient, maxWindowWidth, maxWindowHeight) { + const [width, height] = hyprlandClient.size; + const [xScale, yScale] = [maxWindowWidth / width, maxWindowHeight / height]; + const scale = Math.min(xScale, yScale); + return Qt.size(width * scale, height * scale) +} + +function arrangedClients(hyprlandClients, maxRowWidth, maxWindowWidth, maxWindowHeight) { + const count = hyprlandClients.length; + const resultLayout = []; + + var i = 0; + while (i < count) { + var row = []; + var rowWidth = 0; + var j = i; + + while (j < count) { + const client = hyprlandClients[j]; + const scaledSize = scaleWindow(client, maxWindowWidth, maxWindowHeight); + + if (rowWidth + scaledSize.width <= maxRowWidth || row.length === 0) { + row.push(client); + rowWidth += scaledSize.width; + j++; + } else { + break; + } + } + + resultLayout.push(row); + i = j; + } + + return resultLayout; +} diff --git a/dots/.config/quickshell/ii/panelFamilies/IllogicalImpulseFamily.qml b/dots/.config/quickshell/ii/panelFamilies/IllogicalImpulseFamily.qml new file mode 100644 index 000000000..f4ffda651 --- /dev/null +++ b/dots/.config/quickshell/ii/panelFamilies/IllogicalImpulseFamily.qml @@ -0,0 +1,45 @@ +import QtQuick +import Quickshell + +import qs.modules.common +import qs.modules.ii.background +import qs.modules.ii.bar +import qs.modules.ii.cheatsheet +import qs.modules.ii.dock +import qs.modules.ii.lock +import qs.modules.ii.mediaControls +import qs.modules.ii.notificationPopup +import qs.modules.ii.onScreenDisplay +import qs.modules.ii.onScreenKeyboard +import qs.modules.ii.overview +import qs.modules.ii.polkit +import qs.modules.ii.regionSelector +import qs.modules.ii.screenCorners +import qs.modules.ii.sessionScreen +import qs.modules.ii.sidebarLeft +import qs.modules.ii.sidebarRight +import qs.modules.ii.overlay +import qs.modules.ii.verticalBar +import qs.modules.ii.wallpaperSelector + +Scope { + PanelLoader { extraCondition: !Config.options.bar.vertical; component: Bar {} } + PanelLoader { component: Background {} } + PanelLoader { component: Cheatsheet {} } + PanelLoader { extraCondition: Config.options.dock.enable; component: Dock {} } + PanelLoader { component: Lock {} } + PanelLoader { component: MediaControls {} } + PanelLoader { component: NotificationPopup {} } + PanelLoader { component: OnScreenDisplay {} } + PanelLoader { component: OnScreenKeyboard {} } + PanelLoader { component: Overlay {} } + PanelLoader { component: Overview {} } + PanelLoader { component: Polkit {} } + PanelLoader { component: RegionSelector {} } + PanelLoader { component: ScreenCorners {} } + PanelLoader { component: SessionScreen {} } + PanelLoader { component: SidebarLeft {} } + PanelLoader { component: SidebarRight {} } + PanelLoader { extraCondition: Config.options.bar.vertical; component: VerticalBar {} } + PanelLoader { component: WallpaperSelector {} } +} diff --git a/dots/.config/quickshell/ii/panelFamilies/PanelLoader.qml b/dots/.config/quickshell/ii/panelFamilies/PanelLoader.qml new file mode 100644 index 000000000..1e7b52f42 --- /dev/null +++ b/dots/.config/quickshell/ii/panelFamilies/PanelLoader.qml @@ -0,0 +1,9 @@ +import QtQuick +import Quickshell + +import qs.modules.common + +LazyLoader { + property bool extraCondition: true + active: Config.ready && extraCondition +} diff --git a/dots/.config/quickshell/ii/panelFamilies/WaffleFamily.qml b/dots/.config/quickshell/ii/panelFamilies/WaffleFamily.qml new file mode 100644 index 000000000..67d35de55 --- /dev/null +++ b/dots/.config/quickshell/ii/panelFamilies/WaffleFamily.qml @@ -0,0 +1,44 @@ +import QtQuick +import Quickshell + +import qs.modules.common +import qs.modules.waffle.actionCenter +import qs.modules.waffle.background +import qs.modules.waffle.bar +import qs.modules.waffle.lock +import qs.modules.waffle.notificationCenter +import qs.modules.waffle.notificationPopup +import qs.modules.waffle.onScreenDisplay +// import qs.modules.waffle.overlay +import qs.modules.waffle.polkit +import qs.modules.waffle.screenSnip +import qs.modules.waffle.startMenu +import qs.modules.waffle.sessionScreen +import qs.modules.waffle.taskView + +// Fallbacks +import qs.modules.ii.cheatsheet +import qs.modules.ii.onScreenKeyboard +import qs.modules.ii.overlay +import qs.modules.ii.wallpaperSelector + +Scope { + PanelLoader { component: WaffleActionCenter {} } + PanelLoader { component: WaffleBar {} } + PanelLoader { component: WaffleBackground {} } + PanelLoader { component: WaffleLock {} } + PanelLoader { component: WaffleNotificationCenter {} } + PanelLoader { component: WaffleNotificationPopup {} } + PanelLoader { component: WaffleOSD {} } + // PanelLoader { component: WaffleOverlay {} } + PanelLoader { component: WafflePolkit {} } + PanelLoader { component: WScreenSnip {} } + PanelLoader { component: WaffleStartMenu {} } + PanelLoader { component: WaffleSessionScreen {} } + PanelLoader { component: WaffleTaskView {} } + + PanelLoader { component: Cheatsheet {} } + PanelLoader { component: OnScreenKeyboard {} } + PanelLoader { component: Overlay {} } + PanelLoader { component: WallpaperSelector {} } +} diff --git a/dots/.config/quickshell/ii/scripts/colors/switchwall.sh b/dots/.config/quickshell/ii/scripts/colors/switchwall.sh index 31f260760..430114d5b 100755 --- a/dots/.config/quickshell/ii/scripts/colors/switchwall.sh +++ b/dots/.config/quickshell/ii/scripts/colors/switchwall.sh @@ -319,6 +319,13 @@ main() { get_type_from_config() { jq -r '.appearance.palette.type' "$SHELL_CONFIG_FILE" 2>/dev/null || echo "auto" } + get_accent_color_from_config() { + jq -r '.appearance.palette.accentColor' "$SHELL_CONFIG_FILE" 2>/dev/null || echo "" + } + set_accent_color() { + local color="$1" + jq --arg color "$color" '.appearance.palette.accentColor = $color' "$SHELL_CONFIG_FILE" > "$SHELL_CONFIG_FILE.tmp" && mv "$SHELL_CONFIG_FILE.tmp" "$SHELL_CONFIG_FILE" + } detect_scheme_type_from_image() { local img="$1" @@ -338,12 +345,14 @@ main() { shift 2 ;; --color) - color_flag="1" if [[ "$2" =~ ^#?[A-Fa-f0-9]{6}$ ]]; then - color="$2" + set_accent_color "$2" + shift 2 + elif [[ "$2" == "clear" ]]; then + set_accent_color "" shift 2 else - color=$(hyprpicker --no-fancy) + set_accent_color $(hyprpicker --no-fancy) shift fi ;; @@ -365,6 +374,13 @@ main() { esac done + # If accentColor is set in config, use it + config_color="$(get_accent_color_from_config)" + if [[ "$config_color" =~ ^#?[A-Fa-f0-9]{6}$ ]]; then + color_flag="1" + color="$config_color" + fi + # If type_flag is not set, get it from config if [[ -z "$type_flag" ]]; then type_flag="$(get_type_from_config)" diff --git a/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen-venv.sh b/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen-venv.sh index fc7d8729e..5b24f16f9 100755 --- a/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen-venv.sh +++ b/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen-venv.sh @@ -2,6 +2,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate -"$SCRIPT_DIR/thumbgen.py" "$@" +GIO_USE_VFS=local "$SCRIPT_DIR/thumbgen.py" "$@" deactivate diff --git a/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen.py b/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen.py index 69f630721..92dd126f6 100755 --- a/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen.py +++ b/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen.py @@ -15,7 +15,7 @@ import gi from loguru import logger from tqdm import tqdm -gi.require_version("GnomeDesktop", "3.0") +gi.require_version("GnomeDesktop", "4.0") from gi.repository import Gio, GnomeDesktop # isort:skip thumbnail_size_map = { diff --git a/dots/.config/quickshell/ii/services/Ai.qml b/dots/.config/quickshell/ii/services/Ai.qml index ccd237de8..580b3cfdb 100644 --- a/dots/.config/quickshell/ii/services/Ai.qml +++ b/dots/.config/quickshell/ii/services/Ai.qml @@ -255,19 +255,6 @@ Singleton { // - api_format: The API format of the model. Can be "openai" or "gemini". Default is "openai". // - extraParams: Extra parameters to be passed to the model. This is a JSON object. property var models: Config.options.policies.ai === 2 ? {} : { - "gemini-2.0-flash": aiModelComponent.createObject(this, { - "name": "Gemini 2.0 Flash", - "icon": "google-gemini-symbolic", - "description": Translation.tr("Online | Google's model\nFast, can perform searches for up-to-date information"), - "homepage": "https://aistudio.google.com", - "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent", - "model": "gemini-2.0-flash", - "requires_key": true, - "key_id": "gemini", - "key_get_link": "https://aistudio.google.com/app/apikey", - "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), - "api_format": "gemini", - }), "gemini-2.5-flash": aiModelComponent.createObject(this, { "name": "Gemini 2.5 Flash", "icon": "google-gemini-symbolic", @@ -281,26 +268,13 @@ Singleton { "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", }), - "gemini-2.5-flash-pro": aiModelComponent.createObject(this, { - "name": "Gemini 2.5 Pro", + "gemini-3-flash": aiModelComponent.createObject(this, { + "name": "Gemini 3 Flash", "icon": "google-gemini-symbolic", - "description": Translation.tr("Online | Google's model\nGoogle's state-of-the-art multipurpose model that excels at coding and complex reasoning tasks."), + "description": Translation.tr("Online | Google's model\nPro-level intelligence at the speed and pricing of Flash."), "homepage": "https://aistudio.google.com", - "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent", - "model": "gemini-2.5-pro", - "requires_key": true, - "key_id": "gemini", - "key_get_link": "https://aistudio.google.com/app/apikey", - "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), - "api_format": "gemini", - }), - "gemini-2.5-flash-lite": aiModelComponent.createObject(this, { - "name": "Gemini 2.5 Flash-Lite", - "icon": "google-gemini-symbolic", - "description": Translation.tr("Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput."), - "homepage": "https://aistudio.google.com", - "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent", - "model": "gemini-2.5-flash-lite", + "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:streamGenerateContent", + "model": "gemini-3-flash-preview", "requires_key": true, "key_id": "gemini", "key_get_link": "https://aistudio.google.com/app/apikey", @@ -320,19 +294,6 @@ Singleton { "key_get_description": Translation.tr("**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key"), "api_format": "mistral", }), - "github-gpt-5-nano": aiModelComponent.createObject(this, { - "name": "GPT-5 Nano (GH Models)", - "icon": "github-symbolic", - "api_format": "openai", - "description": Translation.tr("Online via %1 | %2's model").arg("GitHub Models").arg("OpenAI"), - "homepage": "https://github.com/marketplace/models", - "endpoint": "https://models.inference.ai.azure.com/chat/completions", - "model": "gpt-5-nano", - "requires_key": true, - "key_id": "github", - "key_get_link": "https://github.com/settings/tokens", - "key_get_description": Translation.tr("**Pricing**: Free tier available with limited rates. See https://docs.github.com/en/billing/concepts/product-billing/github-models\n\n**Instructions**: Generate a GitHub personal access token with Models permission, then set as API key here\n\n**Note**: To use this you will have to set the temperature parameter to 1"), - }), "openrouter-deepseek-r1": aiModelComponent.createObject(this, { "name": "DeepSeek R1", "icon": "deepseek-symbolic", diff --git a/dots/.config/quickshell/ii/services/Audio.qml b/dots/.config/quickshell/ii/services/Audio.qml index 4b45701a9..68ebc3ae0 100644 --- a/dots/.config/quickshell/ii/services/Audio.qml +++ b/dots/.config/quickshell/ii/services/Audio.qml @@ -18,6 +18,7 @@ Singleton { readonly property real hardMaxValue: 2.00 // People keep joking about setting volume to 5172% so... property string audioTheme: Config.options.sounds.theme property real value: sink?.audio.volume ?? 0 + function friendlyDeviceName(node) { return (node.nickname || node.description || Translation.tr("Unknown")); } diff --git a/dots/.config/quickshell/ii/services/Brightness.qml b/dots/.config/quickshell/ii/services/Brightness.qml index 465d8729c..f2f4f0a7e 100644 --- a/dots/.config/quickshell/ii/services/Brightness.qml +++ b/dots/.config/quickshell/ii/services/Brightness.qml @@ -48,6 +48,16 @@ Singleton { ddcProc.running = true; } + function initializeMonitor(i: int): void { + if (i >= monitors.length) + return; + monitors[i].initialize(); + } + + function ddcDetectFinished(): void { + initializeMonitor(0); + } + Process { id: ddcProc @@ -58,13 +68,13 @@ Singleton { if (data.startsWith("Display ")) { const lines = data.split("\n").map(l => l.trim()); root.ddcMonitors.push({ - model: lines.find(l => l.startsWith("Monitor:")).split(":")[2], + name: lines.find(l => l.startsWith("DRM connector:")).split("-").slice(1).join('-'), busNum: lines.find(l => l.startsWith("I2C bus:")).split("/dev/i2c-")[1] }); } } } - onExited: root.ddcMonitorsChanged() + onExited: root.ddcDetectFinished() } Process { @@ -75,14 +85,8 @@ Singleton { id: monitor required property ShellScreen screen - readonly property bool isDdc: { - const match = root.ddcMonitors.find(m => m.model === screen.model && !root.monitors.slice(0, root.monitors.indexOf(this)).some(mon => mon.busNum === m.busNum)); - return !!match; - } - readonly property string busNum: { - const match = root.ddcMonitors.find(m => m.model === screen.model && !root.monitors.slice(0, root.monitors.indexOf(this)).some(mon => mon.busNum === m.busNum)); - return match?.busNum ?? ""; - } + property bool isDdc + property string busNum property int rawMaxBrightness: 100 property real brightness property real brightnessMultiplier: 1.0 @@ -110,6 +114,9 @@ Singleton { function initialize() { monitor.ready = false; + const match = root.ddcMonitors.find(m => m.name === screen.name && !root.monitors.slice(0, root.monitors.indexOf(this)).some(mon => mon.busNum === m.busNum)); + isDdc = !!match; + busNum = match?.busNum ?? ""; initProc.command = isDdc ? ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] : ["sh", "-c", `echo "a b c $(brightnessctl g) $(brightnessctl m)"`]; initProc.running = true; } @@ -123,6 +130,9 @@ Singleton { monitor.ready = true; } } + onExited: (exitCode, exitStatus) => { + initializeMonitor(root.monitors.indexOf(monitor) + 1); + } } // We need a delay for DDC monitors because they can be quite slow and might act weird with rapid changes @@ -157,14 +167,6 @@ Singleton { function setBrightnessMultiplier(value: real): void { monitor.brightnessMultiplier = value; } - - Component.onCompleted: { - initialize(); - } - - onBusNumChanged: { - initialize(); - } } Component { @@ -215,7 +217,7 @@ Singleton { Process { id: screenshotProc - command: ["bash", "-c", + command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}'` + ` && grim -o '${StringUtils.shellSingleQuoteEscape(screenScope.screenName)}' -` + ` | magick png:- -colorspace Gray -format "%[fx:mean*100]" info:` diff --git a/dots/.config/quickshell/ii/services/DateTime.qml b/dots/.config/quickshell/ii/services/DateTime.qml index 514da212b..e1a8300d4 100644 --- a/dots/.config/quickshell/ii/services/DateTime.qml +++ b/dots/.config/quickshell/ii/services/DateTime.qml @@ -22,7 +22,7 @@ Singleton { property string shortDate: Qt.locale().toString(clock.date, Config.options?.time.shortDateFormat ?? "dd/MM") property string date: Qt.locale().toString(clock.date, Config.options?.time.dateWithYearFormat ?? "dd/MM/yyyy") property string longDate: Qt.locale().toString(clock.date, Config.options?.time.dateFormat ?? "dddd, dd/MM") - property string collapsedCalendarFormat: Qt.locale().toString(clock.date, "dd MMMM yyyy") + property string collapsedCalendarFormat: Qt.locale().toString(clock.date, "dddd, MMMM dd") property string uptime: "0h, 0m" Timer { diff --git a/dots/.config/quickshell/ii/services/HyprlandData.qml b/dots/.config/quickshell/ii/services/HyprlandData.qml index abbaaf577..5ec7bd68d 100644 --- a/dots/.config/quickshell/ii/services/HyprlandData.qml +++ b/dots/.config/quickshell/ii/services/HyprlandData.qml @@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Io +import Quickshell.Wayland import Quickshell.Hyprland /** @@ -21,6 +22,30 @@ Singleton { property var monitors: [] property var layers: ({}) + // Convenient stuff + + function toplevelsForWorkspace(workspace) { + return ToplevelManager.toplevels.values.filter(toplevel => { + const address = `0x${toplevel.HyprlandToplevel?.address}`; + var win = HyprlandData.windowByAddress[address]; + return win?.workspace?.id === workspace; + }) + } + + function hyprlandClientsForWorkspace(workspace) { + return root.windowList.filter(win => win.workspace.id === workspace); + } + + function clientForToplevel(toplevel) { + if (!toplevel || !toplevel.HyprlandToplevel) { + return null; + } + const address = `0x${toplevel?.HyprlandToplevel?.address}`; + return root.windowByAddress[address]; + } + + // Internals + function updateWindowList() { getClients.running = true; } @@ -63,6 +88,7 @@ Singleton { function onRawEvent(event) { // console.log("Hyprland raw event:", event.name); + if (["openlayer", "closelayer", "screencast"].includes(event.name)) return; updateAll() } } diff --git a/dots/.config/quickshell/ii/services/Idle.qml b/dots/.config/quickshell/ii/services/Idle.qml index ad938f3c3..a2b804772 100644 --- a/dots/.config/quickshell/ii/services/Idle.qml +++ b/dots/.config/quickshell/ii/services/Idle.qml @@ -1,8 +1,8 @@ +pragma Singleton import qs.modules.common import QtQuick import Quickshell import Quickshell.Wayland -pragma Singleton /** * A nice wrapper for date and time strings. @@ -17,21 +17,26 @@ Singleton { target: Persistent function onReadyChanged() { if (!Persistent.isNewHyprlandInstance) { - root.inhibit = Persistent.states.idle.inhibit + root.inhibit = Persistent.states.idle.inhibit; } else { - Persistent.states.idle.inhibit = root.inhibit + Persistent.states.idle.inhibit = root.inhibit; } } } - function toggleInhibit() { - root.inhibit = !root.inhibit - Persistent.states.idle.inhibit = root.inhibit + function toggleInhibit(active = null) { + if (active !== null) { + root.inhibit = active; + } else { + root.inhibit = !root.inhibit; + } + Persistent.states.idle.inhibit = root.inhibit; } IdleInhibitor { id: idleInhibitor - window: PanelWindow { // Inhibitor requires a "visible" surface + window: PanelWindow { + // Inhibitor requires a "visible" surface // Actually not lol implicitWidth: 0 implicitHeight: 0 @@ -46,6 +51,5 @@ Singleton { item: null } } - } - + } } diff --git a/dots/.config/quickshell/ii/services/LauncherApps.qml b/dots/.config/quickshell/ii/services/LauncherApps.qml new file mode 100644 index 000000000..0017f3812 --- /dev/null +++ b/dots/.config/quickshell/ii/services/LauncherApps.qml @@ -0,0 +1,41 @@ +pragma Singleton + +import qs.modules.common +import QtQuick +import Quickshell + +Singleton { + id: root + + function isPinned(appId) { + return Config.options.launcher.pinnedApps.indexOf(appId) !== -1; + } + + function togglePin(appId) { + if (root.isPinned(appId)) { + Config.options.launcher.pinnedApps = Config.options.launcher.pinnedApps.filter(id => id !== appId) + } else { + Config.options.launcher.pinnedApps = Config.options.launcher.pinnedApps.concat([appId]) + } + } + + function moveToFront(appId) { + if (!root.isPinned(appId)) return; + const pinnedApps = Config.options.launcher.pinnedApps; + Config.options.launcher.pinnedApps = [appId].concat(pinnedApps.filter(id => id !== appId)); + } + + function moveLeft(appId) { + const pinnedApps = Config.options.launcher.pinnedApps; + const index = pinnedApps.indexOf(appId); + if (index === -1 || index === 0) return; + Config.options.launcher.pinnedApps = pinnedApps.slice(0, index - 1).concat([appId]).concat(pinnedApps[index - 1]).concat(pinnedApps.slice(index + 1)); + } + + function moveRight(appId) { + const pinnedApps = Config.options.launcher.pinnedApps; + const index = pinnedApps.indexOf(appId); + if (index === -1 || index === pinnedApps.length - 1) return; + Config.options.launcher.pinnedApps = pinnedApps.slice(0, index).concat(pinnedApps[index + 1]).concat([appId]).concat(pinnedApps.slice(index + 2)); + } +} diff --git a/dots/.config/quickshell/ii/services/LauncherSearch.qml b/dots/.config/quickshell/ii/services/LauncherSearch.qml new file mode 100644 index 000000000..52e783e28 --- /dev/null +++ b/dots/.config/quickshell/ii/services/LauncherSearch.qml @@ -0,0 +1,362 @@ +pragma Singleton + +import qs.modules.common +import qs.modules.common.models +import qs.modules.common.functions +import QtQuick +import Qt.labs.folderlistmodel +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property string query: "" + + function ensurePrefix(prefix) { + if ([Config.options.search.prefix.action, Config.options.search.prefix.app, Config.options.search.prefix.clipboard, Config.options.search.prefix.emojis, Config.options.search.prefix.math, Config.options.search.prefix.shellCommand, Config.options.search.prefix.webSearch,].some(i => root.query.startsWith(i))) { + root.query = prefix + root.query.slice(1); + } else { + root.query = prefix + root.query; + } + } + + // https://specifications.freedesktop.org/menu/latest/category-registry.html + property list mainRegisteredCategories: ["AudioVideo", "Development", "Education", "Game", "Graphics", "Network", "Office", "Science", "Settings", "System", "Utility"] + property list appCategories: DesktopEntries.applications.values.reduce((acc, entry) => { + for (const category of entry.categories) { + if (!acc.includes(category) && mainRegisteredCategories.includes(category)) { + acc.push(category); + } + } + return acc; + }, []).sort() + + // Load user action scripts from ~/.config/illogical-impulse/actions/ + // Uses FolderListModel to auto-reload when scripts are added/removed + property var userActionScripts: { + const actions = []; + for (let i = 0; i < userActionsFolder.count; i++) { + const fileName = userActionsFolder.get(i, "fileName"); + const filePath = userActionsFolder.get(i, "filePath"); + if (fileName && filePath) { + const actionName = fileName.replace(/\.[^/.]+$/, ""); // strip extension + actions.push({ + action: actionName, + execute: ((path) => (args) => { + Quickshell.execDetached([path, ...(args ? args.split(" ") : [])]); + })(FileUtils.trimFileProtocol(filePath.toString())) + }); + } + } + return actions; + } + + FolderListModel { + id: userActionsFolder + folder: Qt.resolvedUrl(Directories.userActions) + showDirs: false + showHidden: false + sortField: FolderListModel.Name + } + + property var searchActions: [ + { + action: "accentcolor", + execute: args => { + Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--noswitch", "--color", ...(args != '' ? [`${args}`] : [])]); + } + }, + { + action: "dark", + execute: () => { + Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "dark", "--noswitch"]); + } + }, + { + action: "konachanwallpaper", + execute: () => { + Quickshell.execDetached([Quickshell.shellPath("scripts/colors/random/random_konachan_wall.sh")]); + } + }, + { + action: "light", + execute: () => { + Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "light", "--noswitch"]); + } + }, + { + action: "superpaste", + execute: args => { + if (!/^(\d+)/.test(args.trim())) { + // Invalid if doesn't start with numbers + Quickshell.execDetached(["notify-send", Translation.tr("Superpaste"), Translation.tr("Usage: %1superpaste NUM_OF_ENTRIES[i]\nSupply i when you want images\nExamples:\n%1superpaste 4i for the last 4 images\n%1superpaste 7 for the last 7 entries").arg(Config.options.search.prefix.action), "-a", "Shell"]); + return; + } + const syntaxMatch = /^(?:(\d+)(i)?)/.exec(args.trim()); + const count = syntaxMatch[1] ? parseInt(syntaxMatch[1]) : 1; + const isImage = !!syntaxMatch[2]; + Cliphist.superpaste(count, isImage); + } + }, + { + action: "todo", + execute: args => { + Todo.addTask(args); + } + }, + { + action: "wallpaper", + execute: () => { + GlobalStates.wallpaperSelectorOpen = true; + } + }, + { + action: "wipeclipboard", + execute: () => { + Cliphist.wipe(); + } + }, + ] + + // Combined built-in and user actions + property var allActions: searchActions.concat(userActionScripts) + + property string mathResult: "" + property bool clipboardWorkSafetyActive: { + const enabled = Config.options.workSafety.enable.clipboard; + const sensitiveNetwork = (StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords)); + return enabled && sensitiveNetwork; + } + + function containsUnsafeLink(entry) { + if (entry == undefined) + return false; + const unsafeKeywords = Config.options.workSafety.triggerCondition.linkKeywords; + return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords); + } + + Timer { + id: nonAppResultsTimer + interval: Config.options.search.nonAppResultDelay + onTriggered: { + let expr = root.query; + if (expr.startsWith(Config.options.search.prefix.math)) { + expr = expr.slice(Config.options.search.prefix.math.length); + } + mathProc.calculateExpression(expr); + } + } + + Process { + id: mathProc + property list baseCommand: ["qalc", "-t"] + function calculateExpression(expression) { + mathProc.running = false; + mathProc.command = baseCommand.concat(expression); + mathProc.running = true; + } + stdout: SplitParser { + onRead: data => { + root.mathResult = data; + } + } + } + + property list results: { + // Search results are handled here + ////////////////// Skip? ////////////////// + if (root.query == "") + return []; + + ///////////// Special cases /////////////// + if (root.query.startsWith(Config.options.search.prefix.clipboard)) { + // Clipboard + const searchString = StringUtils.cleanPrefix(root.query, Config.options.search.prefix.clipboard); + return Cliphist.fuzzyQuery(searchString).map((entry, index, array) => { + const mightBlurImage = Cliphist.entryIsImage(entry) && root.clipboardWorkSafetyActive; + let shouldBlurImage = mightBlurImage; + if (mightBlurImage) { + shouldBlurImage = shouldBlurImage && (root.containsUnsafeLink(array[index - 1]) || root.containsUnsafeLink(array[index + 1])); + } + const type = `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`; + return resultComp.createObject(null, { + rawValue: entry, + name: StringUtils.cleanCliphistEntry(entry), + verb: "", + type: type, + execute: () => { + Cliphist.copy(entry); + }, + actions: [resultComp.createObject(null, { + name: Translation.tr("Copy"), + iconName: "content_copy", + iconType: LauncherSearchResult.IconType.Material, + execute: () => { + Cliphist.copy(entry); + } + }), resultComp.createObject(null, { + name: Translation.tr("Delete"), + iconName: "delete", + iconType: LauncherSearchResult.IconType.Material, + execute: () => { + Cliphist.deleteEntry(entry); + } + })], + blurImage: shouldBlurImage + }); + }).filter(Boolean); + } else if (root.query.startsWith(Config.options.search.prefix.emojis)) { + // Clipboard + const searchString = StringUtils.cleanPrefix(root.query, Config.options.search.prefix.emojis); + return Emojis.fuzzyQuery(searchString).map(entry => { + const emoji = entry.match(/^\s*(\S+)/)?.[1] || ""; + return resultComp.createObject(null, { + rawValue: entry, + name: entry.replace(/^\s*\S+\s+/, ""), + iconName: emoji, + iconType: LauncherSearchResult.IconType.Text, + verb: Translation.tr("Copy"), + type: Translation.tr("Emoji"), + execute: () => { + Quickshell.clipboardText = entry.match(/^\s*(\S+)/)?.[1]; + } + }); + }).filter(Boolean); + } + + ////////////////// Init /////////////////// + nonAppResultsTimer.restart(); + const mathResultObject = resultComp.createObject(null, { + name: root.mathResult, + verb: Translation.tr("Copy"), + type: Translation.tr("Math result"), + fontType: LauncherSearchResult.FontType.Monospace, + iconName: 'calculate', + iconType: LauncherSearchResult.IconType.Material, + execute: () => { + Quickshell.clipboardText = root.mathResult; + } + }); + const appResultObjects = AppSearch.fuzzyQuery(StringUtils.cleanPrefix(root.query, Config.options.search.prefix.app)).map(entry => { + return resultComp.createObject(null, { + type: Translation.tr("App"), + id: entry.id, + name: entry.name, + iconName: entry.icon, + iconType: LauncherSearchResult.IconType.System, + verb: Translation.tr("Open"), + execute: () => { + if (!entry.runInTerminal) + entry.execute(); + else { + // Probably needs more proper escaping, but this will do for now + Quickshell.execDetached(["bash", '-c', `${Config.options.apps.terminal} -e '${StringUtils.shellSingleQuoteEscape(entry.command.join(' '))}'`]); + } + }, + comment: entry.comment, + runInTerminal: entry.runInTerminal, + genericName: entry.genericName, + keywords: entry.keywords, + actions: entry.actions.map(action => { + return resultComp.createObject(null, { + name: action.name, + iconName: action.icon, + iconType: LauncherSearchResult.IconType.System, + execute: () => { + if (!action.runInTerminal) + action.execute(); + else { + Quickshell.execDetached(["bash", '-c', `${Config.options.apps.terminal} -e '${StringUtils.shellSingleQuoteEscape(action.command.join(' '))}'`]); + } + } + }); + }) + }); + }); + const commandResultObject = resultComp.createObject(null, { + name: StringUtils.cleanPrefix(root.query, Config.options.search.prefix.shellCommand).replace("file://", ""), + verb: Translation.tr("Run"), + type: Translation.tr("Command"), + fontType: LauncherSearchResult.FontType.Monospace, + iconName: 'terminal', + iconType: LauncherSearchResult.IconType.Material, + execute: () => { + let cleanedCommand = root.query.replace("file://", ""); + cleanedCommand = StringUtils.cleanPrefix(cleanedCommand, Config.options.search.prefix.shellCommand); + if (cleanedCommand.startsWith(Config.options.search.prefix.shellCommand)) { + cleanedCommand = cleanedCommand.slice(Config.options.search.prefix.shellCommand.length); + } + Quickshell.execDetached(["bash", "-c", root.query.startsWith('sudo') ? `${Config.options.apps.terminal} fish -C '${cleanedCommand}'` : cleanedCommand]); + } + }); + const webSearchResultObject = resultComp.createObject(null, { + name: StringUtils.cleanPrefix(root.query, Config.options.search.prefix.webSearch), + verb: Translation.tr("Search"), + type: Translation.tr("Web search"), + iconName: 'travel_explore', + iconType: LauncherSearchResult.IconType.Material, + execute: () => { + let query = StringUtils.cleanPrefix(root.query, Config.options.search.prefix.webSearch); + let url = Config.options.search.engineBaseUrl + query; + for (let site of Config.options.search.excludedSites) { + url += ` -site:${site}`; + } + Qt.openUrlExternally(url); + } + }); + const launcherActionObjects = root.allActions.map(action => { + const actionString = `${Config.options.search.prefix.action}${action.action}`; + if (actionString.startsWith(root.query) || root.query.startsWith(actionString)) { + return resultComp.createObject(null, { + name: root.query.startsWith(actionString) ? root.query : actionString, + verb: Translation.tr("Run"), + type: Translation.tr("Action"), + iconName: 'settings_suggest', + iconType: LauncherSearchResult.IconType.Material, + execute: () => { + action.execute(root.query.split(" ").slice(1).join(" ")); + } + }); + } + return null; + }).filter(Boolean); + + //////// Prioritized by prefix ///////// + let result = []; + const startsWithNumber = /^\d/.test(root.query); + const startsWithMathPrefix = root.query.startsWith(Config.options.search.prefix.math); + const startsWithShellCommandPrefix = root.query.startsWith(Config.options.search.prefix.shellCommand); + const startsWithWebSearchPrefix = root.query.startsWith(Config.options.search.prefix.webSearch); + if (startsWithNumber || startsWithMathPrefix) { + result.push(mathResultObject); + } else if (startsWithShellCommandPrefix) { + result.push(commandResultObject); + } else if (startsWithWebSearchPrefix) { + result.push(webSearchResultObject); + } + + //////////////// Apps ////////////////// + result = result.concat(appResultObjects); + + ////////// Launcher actions //////////// + result = result.concat(launcherActionObjects); + + /// Math result, command, web search /// + if (Config.options.search.prefix.showDefaultActionsWithoutPrefix) { + if (!startsWithShellCommandPrefix) + result.push(commandResultObject); + if (!startsWithNumber && !startsWithMathPrefix) + result.push(mathResultObject); + if (!startsWithWebSearchPrefix) + result.push(webSearchResultObject); + } + + return result; + } + + Component { + id: resultComp + LauncherSearchResult {} + } +} diff --git a/dots/.config/quickshell/ii/services/MprisController.qml b/dots/.config/quickshell/ii/services/MprisController.qml index 1dc42da2b..02151f1c1 100644 --- a/dots/.config/quickshell/ii/services/MprisController.qml +++ b/dots/.config/quickshell/ii/services/MprisController.qml @@ -9,12 +9,14 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Mpris +import qs.modules.common /** * A service that provides easy access to the active Mpris player. */ Singleton { id: root; + property list players: Mpris.players.values.filter(player => isRealPlayer(player)); property MprisPlayer trackedPlayer: null; property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null; signal trackChanged(reverse: bool); @@ -23,6 +25,29 @@ Singleton { property var activeTrack; + property bool hasPlasmaIntegration: false + Process { + id: plasmaIntegrationAvailabilityCheckProc + running: true + command: ["bash", "-c", "command -v plasma-browser-integration-host"] + onExited: (exitCode, exitStatus) => { + root.hasPlasmaIntegration = (exitCode === 0); + } + } + function isRealPlayer(player) { + if (!Config.options.media.filterDuplicatePlayers) { + return true; + } + return ( + // Remove unecessary native buses from browsers if there's plasma integration + !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) && !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) && + // playerctld just copies other buses and we don't need duplicates + !player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') && + // Non-instance mpd bus + !(player.dbusName?.endsWith('.mpd') && !player.dbusName.endsWith('MediaPlayer2.mpd'))); + } + + // Original stuff from fox below Instantiator { model: Mpris.players; diff --git a/dots/.config/quickshell/ii/services/Notifications.qml b/dots/.config/quickshell/ii/services/Notifications.qml index 702da0f8a..a45b86007 100644 --- a/dots/.config/quickshell/ii/services/Notifications.qml +++ b/dots/.config/quickshell/ii/services/Notifications.qml @@ -135,8 +135,8 @@ Singleton { property var groupsByAppName: groupsForList(root.list) property var popupGroupsByAppName: groupsForList(root.popupList) - property var appNameList: appNameListForGroups(root.groupsByAppName) - property var popupAppNameList: appNameListForGroups(root.popupGroupsByAppName) + property list appNameList: appNameListForGroups(root.groupsByAppName) + property list popupAppNameList: appNameListForGroups(root.popupGroupsByAppName) // Quickshell's notification IDs starts at 1 on each run, while saved notifications // can already contain higher IDs. This is for avoiding id collisions diff --git a/dots/.config/quickshell/ii/services/PolkitService.qml b/dots/.config/quickshell/ii/services/PolkitService.qml index 56576f4f4..758f0b0b8 100644 --- a/dots/.config/quickshell/ii/services/PolkitService.qml +++ b/dots/.config/quickshell/ii/services/PolkitService.qml @@ -11,6 +11,18 @@ Singleton { property alias active: polkitAgent.isActive property alias flow: polkitAgent.flow property bool interactionAvailable: false + property string cleanMessage: { + if (!root.flow) return ""; + return root.flow.message.endsWith(".") + ? root.flow.message.slice(0, -1) + : root.flow.message + } + property string cleanPrompt: { + const inputPrompt = PolkitService.flow?.inputPrompt.trim() ?? ""; + const cleanedInputPrompt = inputPrompt.endsWith(":") ? inputPrompt.slice(0, -1) : inputPrompt; + const usePasswordChars = !PolkitService.flow?.responseVisible ?? true + return cleanedInputPrompt || (usePasswordChars ? Translation.tr("Password") : Translation.tr("Input")) + } function cancel() { root.flow.cancelAuthenticationRequest() diff --git a/dots/.config/quickshell/ii/services/Privacy.qml b/dots/.config/quickshell/ii/services/Privacy.qml new file mode 100644 index 000000000..a14da7689 --- /dev/null +++ b/dots/.config/quickshell/ii/services/Privacy.qml @@ -0,0 +1,16 @@ +pragma Singleton +pragma ComponentBehavior: Bound +import qs.modules.common +import QtQuick +import Quickshell +import Quickshell.Services.Pipewire + +/** + * Screensharing and mic activity. + */ +Singleton { + id: root + + property bool screenSharing: Pipewire.linkGroups.values.filter(pwlg => pwlg.source.type === PwNodeType.VideoSource).map(pwlg => pwlg.target) + property bool micActive: Pipewire.linkGroups.values.filter(pwlg => pwlg.source.type === PwNodeType.AudioSource && pwlg.target.type === PwNodeType.AudioInStream).map(pwlg => pwlg.target) +} diff --git a/dots/.config/quickshell/ii/services/ResourceUsage.qml b/dots/.config/quickshell/ii/services/ResourceUsage.qml index df823adf7..c513b90ca 100644 --- a/dots/.config/quickshell/ii/services/ResourceUsage.qml +++ b/dots/.config/quickshell/ii/services/ResourceUsage.qml @@ -102,6 +102,10 @@ Singleton { Process { id: findCpuMaxFreqProc + environment: ({ + LANG: "C", + LC_ALL: "C" + }) command: ["bash", "-c", "lscpu | grep 'CPU max MHz' | awk '{print $4}'"] running: true stdout: StdioCollector { diff --git a/dots/.config/quickshell/ii/services/SessionWarnings.qml b/dots/.config/quickshell/ii/services/SessionWarnings.qml new file mode 100644 index 000000000..c698d28c7 --- /dev/null +++ b/dots/.config/quickshell/ii/services/SessionWarnings.qml @@ -0,0 +1,39 @@ +pragma Singleton + +import qs.modules.common +import qs.modules.common.functions +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property bool packageManagerRunning: false + property bool downloadRunning: false + + function refresh() { + packageManagerRunning = false; + downloadRunning = false; + detectPackageManagerProc.running = false; + detectPackageManagerProc.running = true; + detectDownloadProc.running = false; + detectDownloadProc.running = true; + } + + Process { + id: detectPackageManagerProc + command: ["bash", "-c", "pidof pacman yay paru dnf zypper apt apx xbps snap apk yum epsi pikman"] + onExited: (exitCode, exitStatus) => { + root.packageManagerRunning = (exitCode === 0); + } + } + + Process { + id: detectDownloadProc + command: ["bash", "-c", "pidof curl wget aria2c yt-dlp || ls ~/Downloads | grep -E '\.crdownload$|\.part$'"] + onExited: (exitCode, exitStatus) => { + root.downloadRunning = (exitCode === 0); + } + } +} diff --git a/dots/.config/quickshell/ii/services/TaskbarApps.qml b/dots/.config/quickshell/ii/services/TaskbarApps.qml index 052abcaec..6351896b8 100644 --- a/dots/.config/quickshell/ii/services/TaskbarApps.qml +++ b/dots/.config/quickshell/ii/services/TaskbarApps.qml @@ -8,8 +8,12 @@ import Quickshell.Wayland Singleton { id: root + function isPinned(appId) { + return Config.options.dock.pinnedApps.indexOf(appId) !== -1; + } + function togglePin(appId) { - if (Config.options.dock.pinnedApps.indexOf(appId) !== -1) { + if (root.isPinned(appId)) { Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.filter(id => id !== appId) } else { Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.concat([appId]) diff --git a/dots/.config/quickshell/ii/services/Updates.qml b/dots/.config/quickshell/ii/services/Updates.qml index 58b8be892..48549ac3c 100644 --- a/dots/.config/quickshell/ii/services/Updates.qml +++ b/dots/.config/quickshell/ii/services/Updates.qml @@ -13,6 +13,7 @@ Singleton { id: root property bool available: false + property alias checking: checkUpdatesProc.running property int count: 0 readonly property bool updateAdvised: available && count > Config.options.updates.adviseUpdateThreshold diff --git a/dots/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml b/dots/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml index b610c3d44..9f04f6364 100644 --- a/dots/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml +++ b/dots/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml @@ -111,6 +111,14 @@ ApiStrategy { return ({}) } + // Error response handling + if (dataJson.error) { + const errorMsg = `**Error ${dataJson.error.code}**: ${dataJson.error.message}`; + message.rawContent += errorMsg; + message.content += errorMsg; + return { finished: true }; + } + // No candidates? if (!dataJson.candidates) return {}; diff --git a/dots/.config/quickshell/ii/services/ai/MistralApiStrategy.qml b/dots/.config/quickshell/ii/services/ai/MistralApiStrategy.qml index 14cd1a17a..9a37df096 100644 --- a/dots/.config/quickshell/ii/services/ai/MistralApiStrategy.qml +++ b/dots/.config/quickshell/ii/services/ai/MistralApiStrategy.qml @@ -58,8 +58,17 @@ ApiStrategy { // Real stuff try { const dataJson = JSON.parse(cleanData); + + // Error response handling + if (dataJson.error) { + const errorMsg = `**Error**: ${dataJson.error.message || JSON.stringify(dataJson.error)}`; + message.rawContent += errorMsg; + message.content += errorMsg; + return { finished: true }; + } + let newContent = ""; - + const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content; const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content; diff --git a/dots/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml b/dots/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml index 1178c837f..a049abf30 100644 --- a/dots/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml +++ b/dots/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml @@ -49,8 +49,17 @@ ApiStrategy { // Real stuff try { const dataJson = JSON.parse(cleanData); + + // Error response handling + if (dataJson.error) { + const errorMsg = `**Error**: ${dataJson.error.message || JSON.stringify(dataJson.error)}`; + message.rawContent += errorMsg; + message.content += errorMsg; + return { finished: true }; + } + let newContent = ""; - + const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content; const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content; diff --git a/dots/.config/quickshell/ii/shell.qml b/dots/.config/quickshell/ii/shell.qml index b8193812f..a1cb57baa 100644 --- a/dots/.config/quickshell/ii/shell.qml +++ b/dots/.config/quickshell/ii/shell.qml @@ -6,44 +6,22 @@ // Adjust this to make the shell smaller or larger //@ pragma Env QT_SCALE_FACTOR=1 - -import qs.modules.common -import qs.modules.ii.background -import qs.modules.ii.bar -import qs.modules.ii.cheatsheet -import qs.modules.ii.dock -import qs.modules.ii.lock -import qs.modules.ii.mediaControls -import qs.modules.ii.notificationPopup -import qs.modules.ii.onScreenDisplay -import qs.modules.ii.onScreenKeyboard -import qs.modules.ii.overview -import qs.modules.ii.polkit -import qs.modules.ii.regionSelector -import qs.modules.ii.screenCorners -import qs.modules.ii.sessionScreen -import qs.modules.ii.sidebarLeft -import qs.modules.ii.sidebarRight -import qs.modules.ii.overlay -import qs.modules.ii.verticalBar -import qs.modules.ii.wallpaperSelector - -import qs.modules.waffle.actionCenter -import qs.modules.waffle.background -import qs.modules.waffle.bar -import qs.modules.waffle.onScreenDisplay +import "modules/common" +import "services" +import "panelFamilies" import QtQuick import QtQuick.Window import Quickshell import Quickshell.Io import Quickshell.Hyprland -import qs.services ShellRoot { id: root - // Force initialization of some singletons + // Stuff for every panel family + ReloadPopup {} + Component.onCompleted: { MaterialThemeLoader.reapplyTheme() Hyprsunset.load() @@ -54,53 +32,33 @@ ShellRoot { Updates.load() } - // Load enabled stuff - // Well, these loaders only *allow* them to be loaded, to always load or not is defined in each component - // The media controls for example is not loaded if it's not opened - PanelLoader { identifier: "iiBar"; extraCondition: !Config.options.bar.vertical; component: Bar {} } - PanelLoader { identifier: "iiBackground"; component: Background {} } - PanelLoader { identifier: "iiCheatsheet"; component: Cheatsheet {} } - PanelLoader { identifier: "iiDock"; extraCondition: Config.options.dock.enable; component: Dock {} } - PanelLoader { identifier: "iiLock"; component: Lock {} } - PanelLoader { identifier: "iiMediaControls"; component: MediaControls {} } - PanelLoader { identifier: "iiNotificationPopup"; component: NotificationPopup {} } - PanelLoader { identifier: "iiOnScreenDisplay"; component: OnScreenDisplay {} } - PanelLoader { identifier: "iiOnScreenKeyboard"; component: OnScreenKeyboard {} } - PanelLoader { identifier: "iiOverlay"; component: Overlay {} } - PanelLoader { identifier: "iiOverview"; component: Overview {} } - PanelLoader { identifier: "iiPolkit"; component: Polkit {} } - PanelLoader { identifier: "iiRegionSelector"; component: RegionSelector {} } - PanelLoader { identifier: "iiScreenCorners"; component: ScreenCorners {} } - PanelLoader { identifier: "iiSessionScreen"; component: SessionScreen {} } - PanelLoader { identifier: "iiSidebarLeft"; component: SidebarLeft {} } - PanelLoader { identifier: "iiSidebarRight"; component: SidebarRight {} } - PanelLoader { identifier: "iiVerticalBar"; extraCondition: Config.options.bar.vertical; component: VerticalBar {} } - PanelLoader { identifier: "iiWallpaperSelector"; component: WallpaperSelector {} } - PanelLoader { identifier: "wActionCenter"; component: WaffleActionCenter {} } - PanelLoader { identifier: "wBar"; component: WaffleBar {} } - PanelLoader { identifier: "wBackground"; component: WaffleBackground {} } - PanelLoader { identifier: "wOnScreenDisplay"; component: WaffleOSD {} } - ReloadPopup {} - - component PanelLoader: LazyLoader { - required property string identifier - property bool extraCondition: true - active: Config.ready && Config.options.enabledPanels.includes(identifier) && extraCondition - } // Panel families property list families: ["ii", "waffle"] - property var panelFamilies: ({ - "ii": ["iiBar", "iiBackground", "iiCheatsheet", "iiDock", "iiLock", "iiMediaControls", "iiNotificationPopup", "iiOnScreenDisplay", "iiOnScreenKeyboard", "iiOverlay", "iiOverview", "iiPolkit", "iiRegionSelector", "iiScreenCorners", "iiSessionScreen", "iiSidebarLeft", "iiSidebarRight", "iiVerticalBar", "iiWallpaperSelector"], - "waffle": ["wActionCenter", "wBar", "wBackground", "wOnScreenDisplay", "iiCheatsheet", "iiLock", "iiMediaControls", "iiNotificationPopup", "iiOnScreenKeyboard", "iiOverlay", "iiOverview", "iiPolkit", "iiRegionSelector", "iiSessionScreen", "iiSidebarRight", "iiWallpaperSelector"], - }) function cyclePanelFamily() { const currentIndex = families.indexOf(Config.options.panelFamily) const nextIndex = (currentIndex + 1) % families.length Config.options.panelFamily = families[nextIndex] - Config.options.enabledPanels = panelFamilies[Config.options.panelFamily] } + component PanelFamilyLoader: LazyLoader { + required property string identifier + property bool extraCondition: true + active: Config.ready && Config.options.panelFamily === identifier && extraCondition + } + + PanelFamilyLoader { + identifier: "ii" + component: IllogicalImpulseFamily {} + } + + PanelFamilyLoader { + identifier: "waffle" + component: WaffleFamily {} + } + + + // Shortcuts IpcHandler { target: "panelFamily" diff --git a/dots/.config/quickshell/ii/translations/zh_CN.json b/dots/.config/quickshell/ii/translations/zh_CN.json index 432fdd38d..3b4c17d69 100644 --- a/dots/.config/quickshell/ii/translations/zh_CN.json +++ b/dots/.config/quickshell/ii/translations/zh_CN.json @@ -8,8 +8,8 @@ "Su": "日/*keep*/", "%1 characters": "%1 个字符", "**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key": "**价格**:免费。数据用于训练。\n\n**说明**:登录 Google 账户,允许 AI Studio 创建 Google Cloud 项目或其他要求,然后返回并点击获取 API 密钥", - "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key": "**价格**:免费。数据使用政策取决于您的 OpenRouter 账户设置。\n\n**说明**:登录 OpenRouter 账户,在右上角菜单中选择 Keys,点击创建 API 密钥", - ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!": ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!", + "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key": "**价格**:免费。数据使用政策取决于你的 OpenRouter 账户设置。\n\n**说明**:登录 OpenRouter 账户,在右上角菜单中选择 Keys,点击创建 API 密钥", + ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!": "。Zerochan 注意事项:\n- 你需要指定一个颜色\n- 请在 `sidebar.booru.zerochan.username` 配置项内填写你的 Zerochan 用户名。如果不这样做[将可能会被封禁](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!", "No further instruction provided": "未提供进一步说明", "API key set for %1": "已为 %1 设置 API 密钥", "API key:\n\n```txt\n%1\n```": "API 密钥:\n\n```txt\n%1\n```", @@ -26,10 +26,9 @@ "Bluetooth": "蓝牙", "Brightness": "亮度", "Cancel": "取消", - "Cheat sheet": "快捷键表", + "Cheat sheet": "快捷键指南", "Choose model": "选择模型", "Clean stuff | Excellent quality, no NSFW": "清洁内容 | 优秀质量,无 NSFW", - "Clear": "清除", "Clear chat history": "清除聊天记录", "Clear the current list of images": "清除当前图片列表", "Close": "关闭", @@ -51,23 +50,20 @@ "Go to source (%1)": "转到源 (%1)", "Hibernate": "休眠", "Input": "输入", - "Intelligence": "智能体", + "Intelligence": "智能", "Interface": "界面", "Invalid arguments. Must provide `key` and `value`.": "参数无效。必须提供 `key` 和 `value`。", "Jump to current month": "跳转到当前月份", "Keep system awake": "保持系统唤醒", "Large images | God tier quality, no NSFW.": "大尺寸图片 | 顶级质量,无 NSFW", "Large language models": "大语言模型", - "Launch": "启动", "Local Ollama model | %1": "本地 Ollama 模型 | %1", "Lock": "锁定", "Logout": "注销", "Markdown test": "Markdown 测试", "Math result": "数学结果", "No API key set for %1": "未为 %1 设置 API 密钥", - "No audio source": "无音频源", "No media": "无媒体", - "No notifications": "无通知", "Not visible to model": "对模型不可见", "Nothing here!": "这里什么都没有!", "Notifications": "通知", @@ -77,13 +73,11 @@ "Page %1": "第 %1 页", "Reboot": "重启", "Reboot to firmware settings": "重启到固件设置", - "Reload Hyprland & Quickshell": "重新加载 Hyprland 和 Quickshell", + "Reload Hyprland & Quickshell": "重新加载 Hyprland 与 Quickshell", "Run": "运行", - "Run command": "运行命令", "Save": "保存", "Save to Downloads": "保存到下载文件夹", "Search": "搜索", - "Search the web": "在网络上搜索", "Search, calculate or run": "搜索、计算或运行", "Select Language": "选择语言", "Session": "会话", @@ -97,13 +91,13 @@ "Task Manager": "任务管理器", "Task description": "任务描述", "Temperature must be between 0 and 2": "温度必须在 0 到 2 之间", - "Temperature set to %1": "温度设置为 %1", + "Temperature set to %1": "温度已设置为 %1", "Temperature: %1": "温度:%1", "The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "成人向 | 数量巨大,大量 NSFW,质量参差不齐", "The popular one | Best quantity, but quality can vary wildly": "最受欢迎 | 数量最多,但质量参差不齐", "Thinking": "思考中", "Translation goes here...": "翻译结果会显示在这里...", - "Translator": "翻译器", + "Translator": "翻译", "Unfinished": "未完成", "Unknown": "未知", "Unknown Album": "未知专辑", @@ -114,26 +108,23 @@ "Volume": "音量", "Volume mixer": "音量混合器", "Waifus only | Excellent quality, limited quantity": "仅限角色 | 优秀质量,数量有限", - "Waiting for response...": "等待响应...", "Workspace": "工作区", "%1 Safe Storage": "%1 安全存储", "%1 does not require an API key": "%1 不需要 API 密钥", - "%1 queries pending": "%1 个查询等待中", - "%1 | Right-click to configure": "%1 | 右键点击进行配置", - "Invalid API provider. Supported: \n-": "无效的 API 提供商。支持的:\n-", - "Unknown command:": "未知命令:", - "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "输入 /key 开始使用在线模型\nCtrl+O 展开侧边栏\nCtrl+P 将侧边栏分离为窗口", - "Provider set to": "提供商设置为", - "Invalid model. Supported: \n```": "无效模型。支持的:\n```", + "%1 | Right-click to configure": "%1 | 右键以配置", + "Invalid API provider. Supported: \n- ": "无效的 API 提供商。支持的有:\n- ", + "Unknown command: ": "未知命令:", + "Provider set to ": "提供商已设置为 ", + "Invalid model. Supported: \n```\n": "无效模型。支持的有:\n```\n", "Switched to search mode. Continue with the user's request.": "已切换到搜索模式。继续处理用户请求。", - "Enter tags, or \"%1\" for commands": "输入标签,或 \"%1\" 查看命令", - "Online via %1 | %2's model": "通过 %1 在线 | %2 的模型", - "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "没有找到结果。提示:\n- 检查您的标签和 NSFW 设置\n- 如果没有想到标签,请输入页码", + "Enter tags, or \"%1\" for commands": "输入标签,或 “%1” 以查看命令", + "Online via %1 | %2's model": "在线 | 通过 %1 | %2 的模型", + "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "没有找到结果。提示:\n- 检查你的标签和 NSFW 设置\n- 如果还没想到标签,可以直接输入页码", "Settings": "设置", - "Save chat": "保存对话", - "Load chat": "加载对话", + "Save chat": "保存聊天记录", + "Load chat": "加载聊天记录", "or": "或", - "Set the system prompt for the model.": "为模型设置系统提示。", + "Set the system prompt for the model.": "为模型设置系统提示词。", "To Do": "待办", "Calendar": "日历", "Advanced": "高级", @@ -141,11 +132,11 @@ "Services": "服务", "Light": "浅色", "Dark": "深色", - "Fidelity": "保真度", + "Fidelity": "保真", "Fruit Salad": "水果沙拉", "When not fullscreen": "非全屏时", "Choose file": "选择文件", - "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "随机 Konachan SFW 动漫壁纸\n图片保存到 ~/图片/Wallpapers", + "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "随机 Konachan SFW 动漫壁纸\n图片会保存到 ~/Pictures/Wallpapers", "Be patient...": "请耐心等待...", "Tonal Spot": "色调点", "Auto": "自动", @@ -162,7 +153,7 @@ "Pick wallpaper image on your system": "在系统中选择壁纸图片", "No": "否", "AI": "AI", - "Local only": "仅本地", + "Local only": "仅限本地", "Policies": "策略", "Weeb": "二次元", "Closet": "隐藏", @@ -173,14 +164,14 @@ "Style & wallpaper": "样式与壁纸", "Configuration": "配置", "Keybinds": "快捷键", - "Float": "浮动", + "Float": "悬浮", "Hug": "贴合", "illogical-impulse Welcome": "illogical-impulse 欢迎页", "Info": "信息", "Volume limit": "音量限制", "Prevents abrupt increments and restricts volume limit": "防止骤增并限制音量", "Resources": "资源", - "12h am/pm": "12小时 上午/下午", + "12h am/pm": "12小时制 am/pm", "Base URL": "基础 URL", "Audio": "声音", "Networking": "网络", @@ -200,24 +191,22 @@ "24h": "24小时制", "Use Levenshtein distance-based algorithm instead of fuzzy": "使用 Levenshtein 距离算法替代模糊匹配", "System prompt": "系统提示词", - "12h AM/PM": "12小时 AM/PM", - "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)": "如果你经常打错字可能更好用,但结果可能很奇怪,并且可能无法匹配缩写(如 \"GIMP\" 可能搜不到绘图程序)", + "12h AM/PM": "12小时制 AM/PM", + "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)": "如果你经常打错字可能更好用,但结果可能会奇怪,并且可能无法匹配缩写(如 “GIMP” 可能搜不到绘图程序)", "Critical warning": "临界警告", "User agent (for services that require it)": "用户代理(部分服务需要)", - "Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "这些区域可能是图片或屏幕中具有一定包容性的部分。\n可能并不总是准确。\n这是通过本地运行的图像处理算法实现的,没有使用 AI。", "Workspaces shown": "显示的工作区数", "Dark/Light toggle": "深浅色切换", "Dock": "停靠栏", "Weather": "天气", "Pinned on startup": "启动时固定", - "Always show numbers": "总是显示数字", + "Always show numbers": "始终显示数字", "Keyboard toggle": "键盘切换", - "Scale (%)": "缩放比例(%)", + "Scale (%)": "缩放比例(%)", "Overview": "概览", "Rows": "行数", - "Screenshot tool": "截图工具", - "Number show delay when pressing Super (ms)": "按下 Super 时数字显示延迟(ms)", - "Timeout (ms)": "超时时间(ms)", + "Number show delay when pressing Super (ms)": "按下 Super 时的数字显示延迟(毫秒)", + "Timeout (ms)": "显示时间(毫秒)", "Show app icons": "显示应用图标", "Workspaces": "工作区", "Columns": "列数", @@ -226,7 +215,6 @@ "Mic toggle": "麦克风切换", "Hover to reveal": "悬停显示", "Bar": "条栏", - "Show regions of potential interest": "显示可能感兴趣的区域", "Color picker": "取色器", "Help & Support": "帮助与支持", "Discussions": "讨论区", @@ -242,12 +230,11 @@ "Terminal": "终端", "Shell & utilities": "Shell 与工具", "Qt apps": "Qt 应用", - "Force dark mode in terminal": "终端强制使用深色模式", + "Force dark mode in terminal": "强制终端使用深色模式", "Report a Bug": "报告问题", "Issues": "问题追踪", - "Drag or click a region • LMB: Copy • RMB: Edit": "拖动或点击一个区域 • 鼠标左键:复制 • 鼠标右键:编辑", "Current model: %1\nSet it with %2model MODEL": "当前模型:%1\n使用 %2model MODEL 设置", - "Message the model... \"%1\" for commands": "与模型对话... \"%1\" 查看命令", + "Message the model... \"%1\" for commands": "向模型发送消息... “%1” 以查看命令", "The current system prompt is\n\n---\n\n%1": "当前系统提示词为\n\n---\n\n%1", "Model set to %1": "模型已设置为 %1", "Loaded the following system prompt\n\n---\n\n%1": "已加载以下系统提示词\n\n---\n\n%1", @@ -255,16 +242,13 @@ "Save chat to %1": "保存聊天记录到 %1", "Load chat from %1": "从 %1 加载聊天记录", "Load prompt from %1": "从 %1 加载提示词", - "Select output device": "选择输出设备", - "%1 • %2 tasks": "%1 • %2 个任务", - "Online models disallowed\n\nControlled by `policies.ai` config option": "禁止在线模型\n\n由 `policies.ai` 配置项控制", - "Select input device": "选择输入设备", + "%1 • %2 tasks": "%1 • %2 项任务", + "Online models disallowed\n\nControlled by `policies.ai` config option": "已禁止在线模型\n\n由 `policies.ai` 配置项控制", "Low battery": "电量低", "Registration failed. Please inspect manually with the warp-cli command": "注册失败。请使用 warp-cli 命令手动检查", "Code saved to file": "代码已保存到文件", - "Consider plugging in your device": "请考虑连接您的设备", + "Consider plugging in your device": "请考虑为你的设备充电", "Weather Service": "天气服务", - "Please charge!\nAutomatic suspend triggers at %1": "请充电!\n自动挂起将在 %1 时触发", "Cloudflare WARP (1.1.1.1)": "Cloudflare WARP (1.1.1.1)", "Cloudflare WARP": "Cloudflare WARP", "Download complete": "下载完成", @@ -288,43 +272,40 @@ "Fully charged": "已充满电", "Charging:": "充电功率:", "Discharging:": "放电功率:", - "No pending tasks": "没有待办任务", + "No pending tasks": "没有要做的任务", "... and %1 more": "... 还有 %1 个", "Used:": "已用:", "Free:": "可用:", "Total:": "总计:", "Load:": "负载:", - "High": "高", - "Medium": "中", - "Low": "低", + "Medium": "适中", "Tint icons": "图标着色", - "Performance Profile toggle": "性能配置文件切换", - "**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key": "**说明**:登录 Mistral 账户,在侧边栏中选择 Keys,点击创建新密钥", + "Performance Profile toggle": "性能配置切换", + "**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key": "**说明**:登录 Mistral 账户,在侧边栏中选择 Keys,点击 Create new key", "Invalid arguments. Must provide `command`.": "参数无效。必须提供 `command`。", "Thought": "思考", - "Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "在线 | Google 模型\n针对成本效益和高吞吐量优化的 Gemini 2.5 Flash 模型。", - "Online | Google's model\nFast, can perform searches for up-to-date information": "在线 | Google 模型\n速度快,可搜索最新信息", - "Your package manager is running": "您的包管理器正在运行", + "Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "在线 | Google 的模型\n针对成本效益和高吞吐量优化的 Gemini 2.5 Flash 模型。", + "Online | Google's model\nFast, can perform searches for up-to-date information": "在线 | Google 的模型\n速度快,可搜索最新信息", + "Your package manager is running": "你的包管理器正在运行", "Gives the model search capabilities (immediately)": "为模型提供搜索功能(即时)", "Set the tool to use for the model.": "设置模型使用的工具。", "Night Light | Right-click to toggle Auto mode": "夜间模式 | 右键切换自动模式", "Online | %1's model | Delivers fast, responsive and well-formatted answers. Disadvantages: not very eager to do stuff; might make up unknown function calls": "在线 | %1 的模型 | 提供快速、响应迅速且格式良好的答案。缺点:不太积极主动;可能编造未知的函数调用", - "Depends on workspace": "取决于工作区", + "Depends on workspace": "随工作区移动", "Usage: %1tool TOOL_NAME": "用法:%1tool 工具名称", "Tray": "托盘", "Usage: %1save CHAT_NAME": "用法:%1save 聊天名称", "Approve": "批准", - "Depends on sidebars": "取决于侧边栏", - "Commands, edit configs, search.\nTakes an extra turn to switch to search mode if that's needed": "命令、编辑配置、搜索。\n如果需要,会额外执行一次切换到搜索模式", + "Depends on sidebars": "随侧边栏移动", + "Commands, edit configs, search.\nTakes an extra turn to switch to search mode if that's needed": "执行命令、编辑配置、搜索。\n如果需要,会额外执行一次切换到搜索模式", "Up %1": "运行 %1", - "Tool set to: %1": "工具设置为:%1", - "Wallpaper parallax": "壁纸视差", - "Online | Google's model\nGoogle's state-of-the-art multipurpose model that excels at coding and complex reasoning tasks.": "在线 | Google 模型\nGoogle 最先进的多用途模型,在编程和复杂推理任务方面表现卓越。", + "Tool set to: %1": "工具已设置为 %1", + "Online | Google's model\nGoogle's state-of-the-art multipurpose model that excels at coding and complex reasoning tasks.": "在线 | Google 的模型\nGoogle 最先进的多用途模型,在编程和复杂推理任务方面表现卓越。", "Tint app icons": "应用图标着色", - "Preferred wallpaper zoom (%)": "首选壁纸缩放比例 (%)", - "To set an API key, pass it with the %4 command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "要设置 API 密钥,请使用 %4 命令传递\n\n要查看密钥,请在命令中传递 \"get\"
\n\n### 对于 %1:\n\n**链接**:%2\n\n%3", + "Preferred wallpaper zoom (%)": "首选壁纸缩放比例(%)", + "To set an API key, pass it with the %4 command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "要设置 API 密钥,请使用 %4 命令传递\n\n要查看密钥,请在命令中传递 “get”
\n\n### 对于 %1:\n\n**链接**:%2\n\n%3", "No API key\nSet it with /key YOUR_API_KEY": "无 API 密钥\n使用 /key YOUR_API_KEY 设置", - "Total token count\nInput: %1\nOutput: %2": "总令牌数\n输入:%1\n输出:%2", + "Total token count\nInput: %1\nOutput: %2": "总词元数\n输入:%1\n输出:%2", "Disable tools": "禁用工具", "API key is set\nChange with /key YOUR_API_KEY": "API 密钥已设置\n使用 /key YOUR_API_KEY 更改", "Usage: %1load CHAT_NAME": "用法:%1load 聊天名称", @@ -332,17 +313,17 @@ "Temperature\nChange with /temp VALUE": "温度\n使用 /temp VALUE 更改", "Current tool: %1\nSet it with %2tool TOOL": "当前工具:%1\n使用 %2tool TOOL 设置", "There might be a download in progress": "可能有下载正在进行", - "Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers": "在线 | Google 模型\n比前代模型更慢但应该提供更高质量答案的新模型", - "EasyEffects | Right-click to configure": "EasyEffects | 右键配置", + "Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers": "在线 | Google 的模型\n比前代模型更慢但应该提供更高质量答案的新模型", + "EasyEffects | Right-click to configure": "EasyEffects | 右键以配置", "Command rejected by user": "用户拒绝了命令", "Invalid tool. Supported tools:\n- %1": "无效工具。支持的工具:\n- %1", "Keep right sidebar loaded": "保持右侧边栏加载", "Reject": "拒绝", "Enter password": "输入密码", "Automatically hide": "自动隐藏", - "**Pricing**: Free tier available with limited rates. See https://docs.github.com/en/billing/concepts/product-billing/github-models\n\n**Instructions**: Generate a GitHub personal access token with Models permission, then set as API key here\n\n**Note**: To use this you will have to set the temperature parameter to 1": "**定价**:提供免费层,速率有限。详情见 https://docs.github.com/en/billing/concepts/product-billing/github-models\n\n**说明**:生成一个具有 Models 权限的 GitHub 个人访问令牌,并在此处将其设置为 API 密钥\n\n**注意**:使用此提供商时需要将温度参数设置为 1", + "**Pricing**: Free tier available with limited rates. See https://docs.github.com/en/billing/concepts/product-billing/github-models\n\n**Instructions**: Generate a GitHub personal access token with Models permission, then set as API key here\n\n**Note**: To use this you will have to set the temperature parameter to 1": "**定价**:提供免费层级,速率有限。详情见 https://docs.github.com/en/billing/concepts/product-billing/github-models\n\n**说明**:生成一个具有 Models 权限的 GitHub 个人访问令牌,并在此处将其设置为 API 密钥\n\n**注意**:使用此提供商时需要将温度参数设置为 1", "Conflicts with the shell's system tray implementation": "与 Shell 的系统托盘实现冲突", - "Enjoy! You can reopen the welcome app any time with Super+Shift+Alt+/. To open the settings app, hit Super+I": "祝您愉快!随时可以按 Super+Shift+Alt+/ 重新打开欢迎应用。要打开设置应用,请按 Super+I", + "Enjoy! You can reopen the welcome app any time with Super+Shift+Alt+/. To open the settings app, hit Super+I": "祝你愉快!可以随时按 Super+Shift+Alt+/ 重新打开欢迎应用。要打开设置应用,请按 Super+I", "Reset": "重置", "☕ Break: %1 minutes": "☕ 休息:%1 分钟", "Pomodoro": "番茄钟", @@ -356,12 +337,11 @@ "Config file": "配置文件", "Stopwatch": "秒表", "Break": "休息", - "Shell conflicts killer": "终止冲突程序", + "Shell conflicts killer": "冲突终止程序", "Vertical": "垂直", "🔴 Focus: %1 minutes": "🔴 专注:%1 分钟", "System uptime:": "系统运行时间:", "Focus": "专注", - "Open the shell config file.\nIf the button doesn't work or doesn't open in your favorite editor,\nyou can manually open ~/.config/illogical-impulse/config.json": "打开 Shell 配置文件。\n如果按钮无法工作或没有在您喜欢的编辑器中打开,\n可以手动打开 ~/.config/illogical-impulse/config.json", "Attach a file. Only works with Gemini.": "附加文件。仅适用于 Gemini。", "🌿 Long break: %1 minutes": "🌿 长休息:%1 分钟", "Always": "始终", @@ -370,32 +350,31 @@ "Timer": "计时器", "Conflicts with the shell's notification implementation": "与 Shell 的通知实现冲突", "Pause": "暂停", - "Feels like %1": "体感温度:%1", + "Feels like %1": "体感温度 %1", "Lap": "计时", "Welcome app": "欢迎应用", "Corner style": "角落样式", "Language": "语言", - "Select the language for the user interface.\n\"Auto\" will use your system's locale.": "选择用户界面的语言。\n\"自动\" 将使用系统语言环境。", + "Select the language for the user interface.\n\"Auto\" will use your system's locale.": "选择用户界面的语言。\n“自动”将使用系统区域设置。", "Auto (System)": "自动(系统)", "Interface Language": "界面语言", "Paired": "已配对", - "Hit \"/\" to search": "按 \"/\" 搜索", + "Hit \"/\" to search": "按 “/” 以搜索", "Region width": "区域宽度", "Math": "数学", - "When this is off you'll have to click": "关闭后需要点击", + "When this is off you'll have to click": "若关闭则需要点击来打开", "Edit directory": "编辑目录", "Pills": "胶囊", "No active player": "无活动播放器", "Search wallpapers": "搜索壁纸", "Rect": "矩形", - "Make sure your player has MPRIS support\nor try turning off duplicate player filtering": "请确保您的播放器支持 MPRIS\n或尝试关闭重复播放器过滤", + "Make sure your player has MPRIS support\nor try turning off duplicate player filtering": "请确保你的播放器支持 MPRIS\n或尝试关闭重复播放器过滤", "Shell command": "Shell 命令", "Screen round corner": "屏幕圆角", "Password": "密码", "Bluetooth devices": "蓝牙设备", "Wallpaper & Colors": "壁纸与配色", "Details": "详细信息", - "Show clock": "显示时钟", "Connected": "已连接", "Open network portal": "打开网络门户", "Bar style": "条栏样式", @@ -404,32 +383,32 @@ "Value scroll": "滚动调整数值", "Line-separated": "线条分隔", "Region height": "区域高度", - "Pick a wallpaper": "选择壁纸", - "Visualize region": "可视化区域", + "Pick a wallpaper": "挑选壁纸", + "Visualize region": "显示区域", "Bottom": "底部", - "Usage: %1superpaste NUM_OF_ENTRIES[i]\nSupply i when you want images\nExamples:\n%1superpaste 4i for the last 4 images\n%1superpaste 7 for the last 7 entries": "用法:%1superpaste 条目数[i]\n需要图片时加上 i\n示例:\n%1superpaste 4i 获取最近 4 张图片\n%1superpaste 7 获取最近 7 条记录", - "Terminal: Harmony (%)": "终端:协调度 (%)", + "Usage: %1superpaste NUM_OF_ENTRIES[i]\nSupply i when you want images\nExamples:\n%1superpaste 4i for the last 4 images\n%1superpaste 7 for the last 7 entries": "用法:%1superpaste 条目数[i]\n需要图片时加上 i\n示例:\n%1superpaste 4i 粘贴最近 4 张图片\n%1superpaste 7 粘贴最近 7 个条目", + "Terminal: Harmony (%)": "终端:协调度(%)", "Forget": "忘记", "Background": "背景", "Top": "顶部", - "Not all options are available in this app. You should also check the config file by hitting the \"Config file\" button on the topleft corner or opening %1 manually.": "并非所有选项都在此应用中提供。您还应点击左上角的“配置文件”按钮或手动打开 %1 查看配置文件。", - "General": "常规", + "Not all options are available in this app. You should also check the config file by hitting the \"Config file\" button on the topleft corner or opening %1 manually.": "并非所有选项都在此应用中提供。你还应点击左上角的“配置文件”按钮,或手动打开 %1 以查看配置文件。", + "General": "通用", "Right": "右侧", "Utility buttons": "工具按钮", "Quick": "快速", - "Terminal: Foreground boost (%)": "终端:前景增强 (%)", + "Terminal: Foreground boost (%)": "终端:前景增强(%)", "Left": "左侧", - "Tip: right-clicking a group\nalso expands it": "提示:右键点击一个分组\n也可展开", + "Tip: right-clicking a group\nalso expands it": "提示:右键点击一个分组\n也可将其展开", "Change any time later with /dark, /light, /wallpaper in the launcher\nIf the shell's colors aren't changing:\n 1. Open the right sidebar with Super+N\n 2. Click \"Reload Hyprland & Quickshell\" in the top-right corner": "稍后可在启动器中通过 /dark、/light、/wallpaper 随时更改\n如果 Shell 的配色没有变化:\n 1. 用 Super+N 打开右侧边栏\n 2. 点击右上角的“重新加载 Hyprland 与 Quickshell”", "Place at bottom": "放置在底部", - "Allows you to open sidebars by clicking or hovering screen corners regardless of bar position": "无论条栏位置如何,都允许通过点击或悬停屏幕角落打开侧边栏", + "Allows you to open sidebars by clicking or hovering screen corners regardless of bar position": "无论条栏位置,都允许通过点击或悬停在屏幕角落来打开侧边栏", "Positioning": "位置", "Disconnect": "断开连接", "Unknown device": "未知设备", "Make icons pinned by default": "默认固定图标", "Bar & screen": "条栏与屏幕", "Corner open": "角落打开", - "Hover to trigger": "悬停触发", + "Hover to trigger": "悬停以触发", "Terminal: Harmonize threshold": "终端:协调阈值", "Bar position": "条栏位置", "Place the corners to trigger at the bottom": "将触发角落放置在底部", @@ -439,34 +418,305 @@ "Launch on startup": "启动时锁屏", "Also unlock keyring": "同时解锁密钥环", "Tip: Close a window with Super+Q": "提示: 使用 Super+Q 关闭窗口", - "Remember that on most devices one can always hold the power button to force shutdown\nThis only makes it a tiny bit harder for accidents to happen": "请记住,大多数设备仍可以长按电源键进行强制关机\n此选项只会降低误触的可能性而已", - "This is usually safe and needed for your browser and AI sidebar anyway\nMostly useful for those who use lock on startup instead of a display manager that does it (GDM, SDDM, etc.)": "这通常是安全的,并且对浏览器和 AI 侧边栏也是必要的\n主要对那些使用“开机自动锁屏”而不是显示管理器(如 GDM、SDDM 等)执行锁屏的用户有用", - "Show \"Locked\" text": "显示“已锁定”文字", + "Remember that on most devices one can always hold the power button to force shutdown\nThis only makes it a tiny bit harder for accidents to happen": "请记住,大多数设备仍可以通过长按电源键强制关机\n此选项只会略微降低误触的可能性而已", + "This is usually safe and needed for your browser and AI sidebar anyway\nMostly useful for those who use lock on startup instead of a display manager that does it (GDM, SDDM, etc.)": "这通常是安全的,并且对浏览器和 AI 侧边栏也是必要的\n主要对使用“启动时锁屏”而非显示管理器(如 GDM、SDDM 等)执行锁屏的用户有用", + "Show \"Locked\" text": "显示“已锁定”字样", "at": "在", - "Simple digital": "简洁数字", "Style: general": "样式:通用", - "Pick random from this folder": "从此文件随机选择", + "Pick random from this folder": "从此文件夹随机选择", "Back": "返回", "Cancel wallpaper selection": "取消壁纸选择", - "Timeout duration (if not defined by notification) (ms)": "延时时间 (若通知内未定义)(毫秒)", + "Timeout duration (if not defined by notification) (ms)": "显示时间(若通知未指定)(毫秒)", "Enable blur": "启用模糊", - "Material cookie": "Material 曲奇", "Click to toggle light/dark mode\n(applied when wallpaper is chosen)": "点击来切换浅色/深色模式\n(仅在选择壁纸后生效)", - "Use the system file picker instead\nRight-click to make this the default behavior": "改为使用系统文件选择器\n右键点击可设为默认行为", + "Use the system file picker instead\nRight-click to make this the default behavior": "改为使用系统文件选择器\n右键以将此设为默认行为", "Center clock": "居中时钟", - "Press Super+G to toggle appearance": "按下 Super+G 切换准星显示", "Lock screen": "锁屏", - "Crosshair code (in Valorant's format)": "准星代码 (瓦罗兰特格式)", + "Crosshair code (in Valorant's format)": "准星代码(瓦罗兰特格式)", "Random: osu! seasonal": "随机:osu! 季节性壁纸", "Work safety": "安全模式", "Require password to power off/restart": "需要密码来关机或重启", "Random osu! seasonal background\nImage is saved to ~/Pictures/Wallpapers": "随机 osu! 季节性壁纸\n图片会保存到 ~/Pictures/Wallpapers", "Open editor": "打开编辑器", - "Extra wallpaper zoom (%)": "额外壁纸缩放 (%)", + "Extra wallpaper zoom (%)": "额外壁纸缩放(%)", "Security": "安全", "Clock style": "时钟样式", "Style: Blurred": "样式:模糊", - "Crosshair overlay": "射击准星叠加", "Locked": "已锁定", - "Wallpaper safety enforced": "已启用壁纸安全模式" + "Wallpaper safety enforced": "已启用壁纸安全模式", + "Hour hand": "时针", + "Automatic": "自动", + "Language not listed or incomplete translations?\nYou can choose to generate translations for it with Gemini.\n1. Open the left sidebar with Super+A, set model to Gemini (if it isn't already)\n2. Type /key, hit Enter and follow the instructions\n3. Type /key YOUR_API_KEY\n4. Type the locale of your language below and press Generate": "想要的语言不在列表内或翻译不完整?\n你可以选择使用 Gemini 为其生成翻译。\n1. 按 Super+A 打开左侧边栏,将模型设置为 Gemini(如果不已经是了的话)\n2. 输入 /key,按 Enter 然后跟随说明\n3. 输入 /key YOUR_API_KEY\n4. 在下方输入你的语言代码并按下生成", + "Auto styling with Gemini": "使用 Gemini 自动决定样式", + "Audio output | Right-click for volume mixer & device selector": "音频输出 | 右键打开音量合成器与设备选择器", + "Most busy": "最繁杂处", + "Title font": "标题字体", + "Nerd font icons": "Nerd Font 图标", + "Could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "可能是带有一定边界的图片或屏幕部分。结果不一定总是准确。\n这是通过本地的图片处理算法完成的,过程没有 AI 参与。", + "Hollow": "空心", + "Turn on from sunset to sunrise": "在日落到日出期间开启", + "Notes": "笔记", + "It may take a few seconds to update": "可能需要几秒钟来更新", + "Google Lens": "Google 智能镜头", + "Monospace font": "等宽字体", + "Auto, ": "自动,", + "Classic": "经典", + "Font family name (e.g., JetBrains Mono NF)": "字体名称(如 JetBrains Mono NF)", + "Clear all": "全部清除", + "Show aim lines": "显示瞄准线", + "System sound": "系统声音", + "Search for apps": "搜索应用", + "Circle to Search": "圈定即搜", + "Thin": "纤细", + "Content region": "内容区域", + "Bubble": "气泡", + "Enable opening zoom animation": "启用打开时的缩放动画", + "Secured": "安全", + "Wi-Fi": "Wi-Fi", + "Manage my account": "管理我的账户", + "Regenerate": "重新生成", + "%1\nInternet access": "%1\nInternet 访问", + "Font family name (e.g., Space Grotesk)": "字体名称(如 Space Grotesk)", + "Inactive": "未启用", + "Least busy": "最空旷处", + "Constantly rotate": "持续旋转", + "Anti-flashbang (experimental)": "防高亮保护(实验性)", + "Bold": "加粗", + "Open the shell config file\nAlternatively right-click to copy path": "打开 Shell 配置文件\n或右键以复制路径", + "Description font size": "描述字体大小", + "Dot": "圆点", + "Second hand": "秒针", + "Generate translation with Gemini": "使用 Gemini 生成翻译", + "Enable GPS based location": "启用基于 GPS 的位置", + "Microphone": "麦克风", + "Virtual Keyboard": "屏幕键盘", + "Shut down": "关机", + "Digits in the middle": "在中心显示数字", + "If you want to somehow use fingerprint unlock...": "如果你想想办法用指纹解锁的话...", + "Couldn't recognize music": "未识别到歌曲", + "Split buttons": "拆分按键", + "Number style": "数字样式", + "Write something here...\nUse '-' to create copyable bullet points, like this:\n\nSheep fricker\n- 4x Slab\n- 1x Boat\n- 4x Redstone Dust\n- 1x Sticky Piston\n- 1x End Rod\n- 4x Redstone Repeater\n- 1x Redstone Torch\n- 1x Sheep": "在这里写些什么...\n使用 '-' 来创建可复制的列表项,比如这样:\n\n羊羊快♂乐机\n- 4x 半砖\n- 1x 船\n- 4x 红石粉\n- 1x 黏性活塞\n- 1x 末地烛\n- 4x 红石中继器\n- 1x 红石火把\n- 1x 绵羊", + "Network": "网络", + "Overlay: Floating Image": "叠加面板:悬浮图像", + "Unpin from taskbar": "从任务栏取消固定", + "Sound input": "声音输入", + "Not connected": "未连接", + "Use macOS-like symbols for mods keys": "为修饰键使用 macOS 风格的图标", + "Use symbols for mouse": "使用图标表示鼠标键", + "Fonts": "字体", + "Circle": "圈选", + "Wallpaper selector": "壁纸选择器", + "(Plugged in)": "(电源已接通)", + "Sound effects": "声音效果", + "Intensity": "强度", + "Close window": "关闭窗口", + "Video Recording Path": "视频录制路径", + "Speakers (%1): %2": "扬声器 (%1): %2", + "Perhaps what you're listening to is too niche": "也许你听的音乐太小众了", + "Animate time change": "时间变化动画", + "Set FPS limit": "设置帧数限制", + "Identify Music": "识别音乐", + "Focusing": "专注", + "Sound output": "声音输出", + "Type /key to get started with online models\nCtrl+O to expand sidebar\nCtrl+P to pin sidebar\nCtrl+D to detach sidebar": "输入 /key 来开始使用在线模型\nCtrl+O 拓宽侧边栏\nCtrl+P 固定侧边栏\nCtrl+D 分离侧边栏", + "Parallax": "视差效果", + "EasyEffects": "EasyEffects", + "Show only when locked": "仅在锁屏时显示", + "Date style": "日期样式", + "Font family name (e.g., Google Sans Flex)": "字体名称(如 Google Sans Flex)", + "+%1 notifications": "+%1 个通知", + "Hide clipboard images copied from sussy sources": "隐藏剪贴板中来自奇奇怪怪来源的图片", + "Use Hyprlock (instead of Quickshell)": "使用 Hyprlock(而非 Quickshell)", + "Darken screen": "屏幕变暗", + "Generating...\nDon't close this window!": "生成中...\n请勿关闭此窗口!", + "Dial style": "表盘样式", + "Anti-flashbang": "防高亮保护", + "Please charge!\nAutomatic suspend triggers at %1%": "请充电!\n将在电量为 %1% 时自动挂起", + "Battery full": "电量已充满", + "More Bluetooth settings": "更多蓝牙设置", + "Center icons": "图标居中", + "Generate\nTypically takes 2 minutes": "生成\n通常需要 2 分钟", + "Overlay: Crosshair": "叠加面板:准星", + "You can also manually edit cheatsheet.superKey": "你也可以手动编辑 cheatsheet.superKey 配置项", + "Layers": "显示层", + "Sign out": "注销", + "Android": "安卓", + "Recognize music | Right-click to toggle source": "识别音乐 | 右键以切换源", + "Why this is cool:\nFor non-0 values, it won't trigger when you reach the\nscreen corner along the horizontal edge, but it will when\nyou do along the vertical edge": "为什么这很酷:\n对于非 0 的数值,当你沿着水平边缘抵达屏幕角时,它不会触发;\n但当你沿着垂直边缘抵达屏幕角时,它就会触发", + "Locale code, e.g. fr_FR, de_DE, zh_CN...": "语言代码,如 fr_FR、de_DE、zh_CN 等", + "of %1": "共 %1", + "Clock style (locked)": "时钟样式(锁屏时)", + "City name": "城市名", + "Make sure you have songrec installed": "请确保你已安装 songrec", + "Windows": "窗口", + "Power Profile": "电源模式", + "Used for code and terminal": "用于代码和终端", + "Select language": "选择语言", + "File Explorer": "文件资源管理器", + "Saved ": "已保存 ", + "Not secured": "不安全", + "Overlay: General": "叠加面板:通用", + "Keybind font size": "快捷键字体大小", + "Nothing": "空空如也", + "Main font": "主字体", + "End session": "结束专注", + "Listening...": "正在听取...", + "Font family name (e.g., Readex Pro)": "字体名称(如 Readex Pro)", + "Fill": "填充", + "Dark Mode": "深色模式", + "Restart": "重启", + "Dots": "圆点", + "%1 mins": "%1 分钟", + "Font used for Nerd Font icons": "用于 Nerd Font 图标的字体", + "Region selector (screen snipping/Google Lens)": "区域选择器(屏幕截图与 Google 智能镜头)", + "Battery: %1%2": "电池状态:%1%2", + "Click to cycle through power profiles": "点击以循环切换电源模式", + "When the previous option is off and this is on,\nyou can still hover the corner's end to open sidebar,\nand the remaining area can be used for volume/brightness scroll": "当上一个选项为关闭且此项开启时,你仍然\n可以通过悬停在角落的末端来打开侧边栏,\n剩余的区域将可用于滚动调节音量与亮度", + "Widgets": "小组件", + "On-screen keyboard": "屏幕键盘", + "Used for general UI text": "用于通用 UI 文本", + "Line": "线条", + "Replace 󱕐 for \"Scroll ↓\", 󱕑 \"Scroll ↑\", L󰍽 \"LMB\", R󰍽 \"RMB\", 󱕒 \"Scroll ↑/↓\" and ⇞/⇟ for \"Page_↑/↓\"": "如用 󱕐 来表示 “Scroll ↓”,󱕑 “Scroll ↑”,L󰍽 “LMB”,R󰍽 “RMB”,以及 󱕒 “Scroll ↑/↓” 和 ⇞/⇟ 来表示 “Page_↑/↓”", + "Unmuted": "已打开", + "Path copied": "路径已复制", + "Uses Gemini to categorize the wallpaper then picks a preset based on it.\nYou'll need to set Gemini API key on the left sidebar first.\nImages are downscaled for performance, but just to be safe,\ndo not select wallpapers with sensitive information.": "使用 Gemini 对壁纸进行分类,然后根据分类选择一个预设。\n你需要先在左侧边栏设置 Gemini API 密钥。\n图片会被降低分辨率以提高性能, 但为了安全起见,\n请勿选择包含敏感信息的壁纸。", + "Unread indicator: show count": "未读指示器:显示数量", + "RAM": "内存", + "Saving...": "保存中...", + "Illegal increment": "超过最大增量限制", + "\nLMB to enable/disable\nRMB to toggle size\nScroll to swap position": "\n左键以启用/禁用\n右键以切换尺寸\n滚动以交换位置", + "Health:": "电池健康:", + "Display modifiers and keys in multiple keycap (e.g., \"Ctrl + A\" instead of \"Ctrl A\" or \"󰘴 + A\" instead of \"󰘴 A\")": "使用多个“键帽”显示修饰键和按键(如显示为 “Ctrl + A” 而非 “Ctrl A”,或 “󰘴 + A” 而非 “󰘴 A”)", + "Cookie clock settings": "曲奇时钟设置", + "Eye protection": "护眼选项", + "Tooltips": "悬停提示", + "See fewer": "查看更少", + "Click to show": "点击以显示", + "Circle selection": "圈定选区", + "Enter a valid number": "请输入有效的数字", + "Music Recognition": "音乐识别", + "Sounds": "提示音", + "Input device": "输入设备", + "On": "开", + "Hide sussy/anime wallpapers": "隐藏可疑或动漫壁纸", + "Full": "完整", + "Image source": "图像来源", + "Night Light": "夜间模式", + "Digital clock settings": "数字时钟设置", + "e.g. 󰘴 for Ctrl, 󰘵 for Alt, 󰘶 for Shift, etc": "如用 󰘴 来表示 Ctrl,󰘵 来表示 Alt,󰘶 来表示 Shift 等", + "Use system file picker": "使用系统文件选择器", + "Show hidden icons": "显示隐藏的图标", + "Exceeded max allowed": "已超过最高限制", + "Please unplug the charger": "请拔掉充电器", + "Numbers": "数字", + "Example use case: eroge on one workspace, dark Discord window on another": "使用示例:在一个工作区玩小黄游,另一个工作区开着深色的 Discord 窗口", + "Enabled": "已打开", + "Recognize music": "识别音乐", + "Digital": "数字", + "Audio input | Right-click for volume mixer & device selector": "音频输入 | 右键打开音量合成器与设备选择器", + "Use old sine wave cookie implementation": "使用旧版正弦波形曲奇实现", + "Used for displaying numbers": "用于显示数字", + "Music Recognized": "识别到歌曲", + "Numbers font": "数字字体", + "Media": "媒体", + "Quick toggles": "快捷设置", + "Copy path": "复制路径", + "Screenshot Path (leave empty to just copy)": "屏幕截图路径(留空则只复制)", + "Draggable": "可移动", + "Off": "关", + "Super key symbol": "Super 键图标", + "Normal": "正常", + "Scroll to Bottom": "滚动到底部", + "Audio output": "音频输出", + "Use varying shapes for password characters": "使用多样形状显示密码字符", + "Hour marks": "时标", + "Night Light | Right-click to configure": "夜间模式 | 右键以配置", + "Edit quick toggles": "编辑快捷设置", + "Total duration timeout (s)": "总持续时长(秒)", + "Font family name": "字体名称", + "Rectangular selection": "矩形选区", + "Sides": "边数", + "Stroke width": "笔画粗细", + "Widget: Clock": "小组件:时钟", + "Audio input": "音频输入", + "Polling interval (s)": "轮询间隔(秒)", + "Used for decorative/expressive text": "用于装饰性或富有表现力的文字", + "Enable translator": "启用翻译", + "Pin to taskbar": "固定到任务栏", + "Active": "已启用", + "Used for headings and titles": "用于标题和副标题", + "More volume settings": "更多音量设置", + "Minute hand": "分针", + "Enable if you want clocks to show seconds accurately": "启用以让时钟精准显示秒数", + "Keep awake": "保持唤醒", + "Local account": "本地账户", + "Save paths": "保存路径", + "Open recordings folder": "打开录像文件夹", + "Muted": "已静音", + "Sliders": "滑块", + "CPU": "CPU", + "Second precision": "精确显秒", + "Border": "内圈", + "Reading font": "阅读字体", + "Press Super+G to open the overlay and pin the crosshair": "按 Super+G 来打开叠加面板,然后固定准星", + "Show": "显示", + "More Internet settings": "更多 Internet 设置", + "Get the latest features and security improvements with\nthe newest feature update.\n\n%1 packages": "通过安装更新获取最新的功能和\n安全改进。\n\n%1 个软件包", + "Quote": "语录", + "Widget: Weather": "小组件:天气", + "Used for reading large blocks of text": "用于阅读大段文字", + "with vertical offset": "使用垂直偏移", + "Han chars": "汉字", + "e.g. 󱊫 for F1, 󱊶 for F12": "如用 󱊫 来表示 F1,󱊶 来表示 F12 等", + "Internet": "网络", + "Show notifications": "显示通知", + "Force hover open at absolute corner": "强制在绝对角落悬停打开", + "Use symbols for function keys": "使用符号表示功能键", + "Record": "屏幕录制", + "Authentication": "身份验证", + "Hint target regions": "建议目标区域", + "Enable now": "现在启用", + "You'll need to enter your Gemini API key first.\nType /key on the sidebar for instructions.": "你需要先输入 Gemini API 密钥。\n在侧边栏输入 /key 以获取说明。", + "Output device": "输出设备", + "Swap": "虚拟内存", + "Full warning": "满电警告", + "Padding": "额外边距", + "Expressive font": "表现力字体", + "Balance brightness based on content": "根据内容更改亮度", + "Cookie": "曲奇", + "Fahrenheit unit": "华氏度单位", + "Roman": "罗马", + "Polling interval (m)": "轮询间隔(分钟)", + "Close all windows": "关闭所有窗口", + "Adjust the color temperature": "调整色温", + "Task View": "任务视图", + "More comfortable viewing at night": "夜间浏览更舒适", + "No new notifications": "没有新通知", + "All": "全部", + "Move to front": "移到前面", + "Emoji": "表情符号", + "Best match": "最佳匹配", + "Other": "其他", + "Unpin from Start": "从“开始”屏幕取消固定", + "Polkit": "Polkit", + "Productivity": "效率", + "Web": "网页", + "Apps": "应用", + "Manage accounts": "管理账户", + "Commands": "命令", + "Do you want to allow this app to make changes to your device?": "你要允许此应用对你的设备进行更改吗?", + "Actions": "操作", + "Open": "打开", + "Pinned": "已固定", + "Move right": "右移", + "Command": "命令", + "Utilities & Tools": "实用工具", + "Change password": "更改密码", + "Command-line-invoked Action": "由命令行执行的操作", + "Unknown Application": "未知应用", + "No applications": "没有应用", + "Creativity": "创意", + "Move left": "左移", + "Pin to Start": "固定到“开始”屏幕" } \ No newline at end of file diff --git a/sdata/deps-info.md b/sdata/deps-info.md index fc6f315a3..9a1e4e826 100644 --- a/sdata/deps-info.md +++ b/sdata/deps-info.md @@ -191,6 +191,9 @@ Tips: - Used in Quickshell config. - `wlogout` - Used in Hyprland config. +- `libqalculate` + - Used in Quickshell config, providing math ability in searchbar. + - Note that `qalc` is the needed executable. In Arch Linux [libqalculate](https://archlinux.org/packages/extra/x86_64/libqalculate) provides it, but in Fedora [qalculate](https://packages.fedoraproject.org/pkgs/libqalculate/qalculate/fedora-43.html#files) does and [libqalculate](https://packages.fedoraproject.org/pkgs/libqalculate/libqalculate/fedora-43.html#files) does not. # Actual packages diff --git a/sdata/dist-arch/illogical-impulse-widgets/PKGBUILD b/sdata/dist-arch/illogical-impulse-widgets/PKGBUILD index 76f71d43f..fa9d4df02 100644 --- a/sdata/dist-arch/illogical-impulse-widgets/PKGBUILD +++ b/sdata/dist-arch/illogical-impulse-widgets/PKGBUILD @@ -1,6 +1,6 @@ pkgname=illogical-impulse-widgets pkgver=1.0 -pkgrel=5 +pkgrel=6 pkgdesc='Illogical Impulse Widget Dependencies' arch=(any) license=(None) @@ -14,4 +14,5 @@ depends=( songrec translate-shell wlogout + libqalculate ) diff --git a/sdata/dist-arch/install-deps.sh b/sdata/dist-arch/install-deps.sh index 3cd43d6ba..ecbb3b907 100644 --- a/sdata/dist-arch/install-deps.sh +++ b/sdata/dist-arch/install-deps.sh @@ -43,6 +43,11 @@ if ! command -v pacman >/dev/null 2>&1; then exit 1 fi +# Keep makepkg from resetting sudo credentials +if [[ -z "${PACMAN_AUTH:-}" ]]; then + export PACMAN_AUTH="sudo" +fi + showfun remove_deprecated_dependencies v remove_deprecated_dependencies diff --git a/sdata/dist-fedora/SPECS/quickshell-git.spec b/sdata/dist-fedora/SPECS/quickshell-git.spec index e90bdebe2..5167a1ae3 100644 --- a/sdata/dist-fedora/SPECS/quickshell-git.spec +++ b/sdata/dist-fedora/SPECS/quickshell-git.spec @@ -63,8 +63,6 @@ Wayland and X11. %endif -DBUILD_SHARED_LIBS=OFF \ -DCMAKE_BUILD_TYPE=Release \ - -DDISTRIBUTOR="Fedora COPR (errornointernet/quickshell)" \ - -DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES \ -DGIT_REVISION=%{commit} \ -DINSTALL_QML_PREFIX=%{_lib}/qt6/qml %cmake_build diff --git a/sdata/dist-fedora/feddeps.toml b/sdata/dist-fedora/feddeps.toml index f21169a1b..def14e066 100644 --- a/sdata/dist-fedora/feddeps.toml +++ b/sdata/dist-fedora/feddeps.toml @@ -1,3 +1,13 @@ +# COPR repositories list +[copr] +repos = [ + "ririko66z/dots-hyprland", + "solopasha/hyprland", + "deltacopy/darkly", + "alternateved/eza", + "atim/starship" +] + # Fedora dependencies list # Audio @@ -17,6 +27,7 @@ packages = [ "brightnessctl", "ddcutil" ] +install_opts = ["--setopt=install_weak_deps=False"] # Basic [groups.basic] @@ -181,6 +192,7 @@ packages = [ "hyprpicker", "songrec", "translate-shell", + "qalculate", "wlogout" ] @@ -191,4 +203,5 @@ packages = [ "plasma-systemmonitor", "unzip" ] -install_opts = ["--setopt=install_weak_deps=False"] \ No newline at end of file +install_opts = ["--setopt=install_weak_deps=False"] + diff --git a/sdata/dist-fedora/install-deps.sh b/sdata/dist-fedora/install-deps.sh index 20e1d1328..18da73e06 100644 --- a/sdata/dist-fedora/install-deps.sh +++ b/sdata/dist-fedora/install-deps.sh @@ -1,9 +1,15 @@ # This script is meant to be sourced. # It's not for directly running. +# ------------------------- +# CONFIG +# ------------------------- +user_config="${REPO_ROOT}/sdata/dist-fedora/user_data.yaml" +rpmbuildroot="${REPO_ROOT}/cache/rpmbuild" +deps_data_file="${REPO_ROOT}/sdata/dist-fedora/feddeps.toml" - -# Initialize the user configuration file -user_config=${REPO_ROOT}/sdata/dist-fedora/user_data.yaml +# ------------------------- +# FUNCTIONS +# ------------------------- # Recording DNF Transaction ID function r() { @@ -16,6 +22,42 @@ function r() { [ "$original_id" == "$last_id" ] || yq -i ".dnf.transaction_ids += [ $last_id ]" "$user_config" || : } +# Start building and install the missing RPM package locally. +function install_RPMS() { + local local_specs local_rpms + rpmbuildroot="${rpmbuildroot:-${REPO_ROOT}/cache/rpmbuild}" + + x mkdir -p "$rpmbuildroot"/{BUILD,RPMS,SOURCES} + x cp -r "${REPO_ROOT}/sdata/dist-fedora/SPECS" "$rpmbuildroot/" + + x cd $rpmbuildroot/SPECS + + mapfile -t -d '' local_specs < <(find "$rpmbuildroot/SPECS" -maxdepth 1 -type f -name "*.spec" -print0) + for spec_file in ${local_specs[@]}; do + # Download sources + x spectool -g -C "$rpmbuildroot/SOURCES" "$spec_file" + # Install build dependencies + r x sudo dnf builddep -y "$spec_file" + # Build the RPM package locally. If it fails, download it from COPR. + if ! rpmbuild -bb --define "_topdir $rpmbuildroot" --define "debug_package %{nil}" "$spec_file"; then + printf "${STY_RED}Local build encountered an issue. Downloading $(basename "$spec_file" .spec) from COPR. Report the issue to Discussions pls.${STY_RST}\n" + sudo dnf install -y $(basename "$spec_file" .spec) + nolock_qs=true + fi + done + + mapfile -t -d '' local_rpms < <(find "$rpmbuildroot/RPMS" -maxdepth 2 -type f -name '*.rpm' -not -name '*debug*' -print0) + if [[ ${#local_rpms[@]} -ge 1 ]]; then + echo -e "${STY_BLUE}Next command:${STY_RST} sudo dnf install ${local_rpms[@]} -y" + r x sudo dnf install "${local_rpms[@]}" -y + fi + x cd ${REPO_ROOT} +} + +# ------------------------- +# MAIN +# ------------------------- + if ! command -v dnf >/dev/null 2>&1; then printf "${STY_RED}[$0]: dnf not found, it seems that the system is not Fedora 42 or later distros. Aborting...${STY_RST}\n" exit 1 @@ -33,61 +75,38 @@ v sudo dnf versionlock delete quickshell-git 2>/dev/null # Install yq for parsing config files v sudo dnf install yq -y -# Development-tools -r v sudo dnf install @development-tools fedora-packager rpmdevtools fonts-rpm-macros qt6-rpm-macros -y +# Install development tools +r v sudo dnf install @development-tools fedora-packager -y -# COPR repositories -v sudo dnf copr enable ririko66z/dots-hyprland -y -v sudo dnf copr enable solopasha/hyprland -y -v sudo dnf copr enable deltacopy/darkly -y -v sudo dnf copr enable alternateved/eza -y -v sudo dnf copr enable atim/starship -y - -# Start building and install the missing RPM package locally. -install_RPMS() { - rpmbuildroot=${REPO_ROOT}/cache/rpmbuild - x mkdir -p $rpmbuildroot/{BUILD,RPMS,SOURCES} - x cp -r ${REPO_ROOT}/sdata/dist-fedora/SPECS $rpmbuildroot/ - x cd $rpmbuildroot/SPECS - mapfile -t -d '' local_specs < <(find "$rpmbuildroot/SPECS" -maxdepth 1 -type f -name "*.spec" -print0) - for spec_file in ${local_specs[@]}; do - x spectool -g -C "$rpmbuildroot/SOURCES" $spec_file - r x sudo dnf builddep -y $spec_file - x rpmbuild -bb --define "_topdir $rpmbuildroot" $spec_file - done - mapfile -t -d '' local_rpms < <(find "$rpmbuildroot/RPMS" -maxdepth 2 -type f -name '*.rpm' -not -name '*debug*' -print0) - echo -e "${STY_BLUE}Next command:${STY_RST} sudo dnf install ${local_rpms[@]} -y" - r x sudo dnf install "${local_rpms[@]}" -y - x cd ${REPO_ROOT} -} +# Install COPR repositories +copr_repos_json=$(yq -o=j '.copr.repos // []' "$deps_data_file") +eval "$(jq -r '@sh "copr_repos_array+=(\(.[]))"' <<<"$copr_repos_json")" # Fedora distro contains jq +for copr in ${copr_repos_array[@]}; do + v sudo dnf copr enable "$copr" -y +done +# Build and install locally RPMS showfun install_RPMS v install_RPMS -deps_data_file="${REPO_ROOT}/sdata/dist-fedora/feddeps.toml" +# Install packages from toml file deps_data=$(yq -o=j '.' "$deps_data_file") echo "Starting to install packages from $deps_data_file ..." while IFS= read -r deps_list_key; do - echo "Installing package list: $deps_list_key" + install_opts=$(echo $deps_data | yq ".groups.\"$deps_list_key\" | select(has(\"install_opts\")) | .install_opts[]") package_list=$(echo $deps_data | yq ".groups.\"$deps_list_key\".packages | unique | .[]") r v sudo dnf install -y $install_opts $package_list 0) -') +done < <(echo "$deps_data" | yq '.groups | keys[]? | select(length > 0)') # Add back versionlock at the end -v sudo dnf versionlock add quickshell-git +[ -n $nolock_qs ] || v sudo dnf versionlock add quickshell-git || true echo -e "\n========================================" -echo "All installations are complete." +echo "All installations are completed." echo "========================================" - diff --git a/sdata/dist-gentoo/README.md b/sdata/dist-gentoo/README.md index 630a3a178..84153c779 100644 --- a/sdata/dist-gentoo/README.md +++ b/sdata/dist-gentoo/README.md @@ -52,3 +52,29 @@ end - The Hyprland live ebuild sometimes has linkage issues, deleting _Hyprland_ and _hyprland_ from `/usr/bin/` and then re-emerging usually fixes this. - When emerging Hyprland if you get an issue relating to `undefined reference to ``Hyprutils::Math::Vector2D::˜Vector2D()`` ` - Clear the cache folder (`rm -fr /var/tmp/portage/gui-wm/hyprland*`) then try again +- If emerging ``hyprland-qtutils`` fails and gives you something like this... + ```cmake + CMake Error at utils/dialog/CMakeLists.txt:26 (target_link_libraries): + Target "hyprland-dialog" links to: + Qt6::WaylandClientPrivate + but the target was not found. Possible reasons include: + * There is a typo in the target name. + * A find_package call is missing for an IMPORTED target. + * An ALIAS target is missing. + CMake Error at utils/update-screen/CMakeLists.txt:34 (target_link_libraries): + Target "hyprland-update-screen" links to: + Qt6::WaylandClientPrivate + but the target was not found. Possible reasons include: + * There is a typo in the target name. + * A find_package call is missing for an IMPORTED target. + * An ALIAS target is missing. + CMake Error at utils/donate-screen/CMakeLists.txt:32 (target_link_libraries): + Target "hyprland-donate-screen" links to: + Qt6::WaylandClientPrivate + but the target was not found. Possible reasons include: + * There is a typo in the target name. + * A find_package call is missing for an IMPORTED target. + * An ALIAS target is missing. + ``` + Try putting ``sdata/dist-gentoo/hyprland-qtutils-private.patch`` into ``/etc/portage/patches/gui-libs/hyprland-qtutils/``. + - Patch Credit: fedeliallalinea on https://forums.gentoo.org/viewtopic-p-8874098.html diff --git a/sdata/dist-gentoo/hyprland-qtutils-private.patch b/sdata/dist-gentoo/hyprland-qtutils-private.patch new file mode 100644 index 000000000..6ca55104f --- /dev/null +++ b/sdata/dist-gentoo/hyprland-qtutils-private.patch @@ -0,0 +1,33 @@ +--- a/utils/dialog/CMakeLists.txt ++++ b/utils/dialog/CMakeLists.txt +@@ -8,7 +8,7 @@ + set(CMAKE_CXX_STANDARD 23) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + +-find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient) ++find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient WaylandClientPrivate) + find_package(PkgConfig REQUIRED) + + pkg_check_modules(hyprutils REQUIRED IMPORTED_TARGET hyprutils) +--- a/utils/donate-screen/CMakeLists.txt ++++ b/utils/donate-screen/CMakeLists.txt +@@ -8,7 +8,7 @@ + set(CMAKE_CXX_STANDARD 23) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + +-find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient) ++find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient WaylandClientPrivate) + find_package(PkgConfig REQUIRED) + + pkg_check_modules(hyprutils REQUIRED IMPORTED_TARGET hyprutils) +--- a/utils/update-screen/CMakeLists.txt ++++ b/utils/update-screen/CMakeLists.txt +@@ -8,7 +8,7 @@ + set(CMAKE_CXX_STANDARD 23) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + +-find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient) ++find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient WaylandClientPrivate) + find_package(PkgConfig REQUIRED) + + pkg_check_modules(hyprutils REQUIRED IMPORTED_TARGET hyprutils) diff --git a/sdata/dist-gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r3.ebuild b/sdata/dist-gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r4.ebuild similarity index 95% rename from sdata/dist-gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r3.ebuild rename to sdata/dist-gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r4.ebuild index 95ffeffba..9ffceea09 100644 --- a/sdata/dist-gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r3.ebuild +++ b/sdata/dist-gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r4.ebuild @@ -21,6 +21,7 @@ RDEPEND=" app-misc/songrec app-i18n/translate-shell gui-apps/wlogout + sci-libs/libqalculate " ##### CUSTOM EBUILDS # app-misc/songrec diff --git a/sdata/dist-gentoo/install-deps.sh b/sdata/dist-gentoo/install-deps.sh index 0aadbf81b..9d79c157a 100644 --- a/sdata/dist-gentoo/install-deps.sh +++ b/sdata/dist-gentoo/install-deps.sh @@ -1,23 +1,12 @@ printf "${STY_YELLOW}" printf "============WARNING/NOTE (1)============\n" -printf "GCC in use: $(which gcc)\n" -printf "GCC version info: $(gcc --version | grep gcc)\n" -printf "GCC version number: $(gcc --version | grep gcc | awk '{print $3}')\n" -printf "GCC-15>= is required for Hyprland\n" -printf "If you have GCC-15>= and it's currently set then you can safely ignore this\n" -printf "If not, you must ensure you are using the correct GCC version and set it (gcc-config )\n" -printf "It is heavily recommended to re-emerge @world with an empty tree after changing GCC version (emerge -e @world)\n\n" -printf "${STY_RST}" -pause - -printf "${STY_YELLOW}" -printf "============WARNING/NOTE (2)============\n" printf "Ensure you have a global use flag for elogind or systemd in your make.conf for simplicity\n" printf "Or you can manually add the use flags for each package that requires it\n" printf "${STY_RST}" pause printf "${STY_YELLOW}" +printf "============WARNING/NOTE (2)============\n" printf "https://github.com/end-4/dots-hyprland/blob/main/sdata/dist-gentoo/README.md\n" printf "Checkout the above README for potential bug fixes or additional information\n\n" printf "${STY_RST}" diff --git a/sdata/dist-gentoo/uninstall-deps.sh b/sdata/dist-gentoo/uninstall-deps.sh new file mode 100644 index 000000000..280433946 --- /dev/null +++ b/sdata/dist-gentoo/uninstall-deps.sh @@ -0,0 +1,8 @@ +# This script is meant to be sourced. +# It's not for directly running. + +for i in illogical-impulse-{quickshell-git,audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,oneui4-icons-git,portal,python,screencapture,toolkit,widgets}; do + v sudo emerge --unmerge $i +done + +v sudo emerge --depclean diff --git a/sdata/dist-nix/README.md b/sdata/dist-nix/README.md index 3a8120e04..9aca1d13f 100644 --- a/sdata/dist-nix/README.md +++ b/sdata/dist-nix/README.md @@ -37,8 +37,38 @@ As [commented](https://github.com/end-4/dots-hyprland/issues/1061#issuecomment-3 See also [caelestia-dots/shell#668](https://github.com/caelestia-dots/shell/issues/668). -### NixGL -On non-NixOS distros, packages installed via home-manager have problem accessing GPU, especially Hyprland because it requires GPU acceleration to launch. `nixGL` should be used to address the problem. +### GPU +On non-NixOS distros, packages installed via home-manager have problem accessing GPU, especially Hyprland because it requires GPU acceleration to launch. + +~~`nixGL` should be used to address the problem.~~ + +Since home-manager 25.11, for non-NixOS just set the following: +```nix +targets.genericLinux.enable = true; +``` +Then during building, home-manager will show a message to tell you running a command manually to configure GPU, like: +```bash +sudo /nix/store/-non-nixos-gpu/bin/non-nixos-gpu-setup +``` +It runs a bash script with following content: +``` +#!/nix/store/-bash-/bin/bash + +set -e + +# Install the systemd service file and ensure that the store path won't be +# garbage-collected as long as it's installed. +unit_path=/etc/systemd/system/non-nixos-gpu.service +ln -sf /nix/store/-non-nixos-gpu/resources/non-nixos-gpu.service "$unit_path" +ln -sf "$unit_path" "/nix/var/nix"/gcroots/non-nixos-gpu.service + +systemctl daemon-reload +systemctl enable non-nixos-gpu.service +systemctl restart non-nixos-gpu.service +``` +_Note: it uses `systemctl`, maybe won't work for OpenRC..._ + +See [gpu-non-nixos](https://nix-community.github.io/home-manager/index.xhtml#sec-usage-gpu-non-nixos). # Handling dot files ## Status diff --git a/sdata/dist-nix/home-manager/flake.nix b/sdata/dist-nix/home-manager/flake.nix index d88daad6e..97a50c466 100644 --- a/sdata/dist-nix/home-manager/flake.nix +++ b/sdata/dist-nix/home-manager/flake.nix @@ -3,23 +3,24 @@ description = "illogical-impulse"; inputs = { - # Qt 6.10 is not yet available from released version of nixpkgs. - #nixpkgs.url = "nixpkgs/nixos-25.05"; - nixpkgs.url = "nixpkgs/nixos-unstable"; + nixpkgs.url = "nixpkgs/nixos-25.11"; + #nixpkgs.url = "nixpkgs/nixos-unstable"; home-manager = { - #url = "github:nix-community/home-manager/release-25.05"; - url = "github:nix-community/home-manager/master"; + url = "github:nix-community/home-manager/release-25.11"; + #url = "github:nix-community/home-manager/master"; inputs.nixpkgs.follows = "nixpkgs"; }; - nixgl.url = "github:nix-community/nixGL"; + #nixgl.url = "github:nix-community/nixGL"; quickshell = { url = "github:quickshell-mirror/quickshell/db1777c20b936a86528c1095cbcb1ebd92801402"; inputs.nixpkgs.follows = "nixpkgs"; }; }; - outputs = { nixpkgs, home-manager, nixgl, quickshell, ... }: + outputs = { nixpkgs, home-manager, + #nixgl, + quickshell, ... }: let home_attrs = rec { username = import ./username.nix; @@ -36,7 +37,9 @@ homeConfigurations = { illogical_impulse = home-manager.lib.homeManagerConfiguration { inherit pkgs; - extraSpecialArgs = { inherit home_attrs nixgl quickshell; }; + extraSpecialArgs = { inherit home_attrs + #nixgl + quickshell; }; modules = [ ./home.nix ]; diff --git a/sdata/dist-nix/home-manager/home.nix b/sdata/dist-nix/home-manager/home.nix index 596430395..b1381481d 100644 --- a/sdata/dist-nix/home-manager/home.nix +++ b/sdata/dist-nix/home-manager/home.nix @@ -1,8 +1,13 @@ -{ config, lib, pkgs, nixgl, quickshell, home_attrs, ... }: +{ config, lib, pkgs, +#nixgl, +quickshell, home_attrs, ... }: { programs.home-manager.enable = true; - nixGL.packages = nixgl.packages; - nixGL.defaultWrapper = "mesa"; + + # Necessary for non-NixOS to handle GPU (since home-manager version 25.11) + targets.genericLinux.enable = true; + #nixGL.packages = nixgl.packages; + #nixGL.defaultWrapper = "mesa"; xdg.portal = { enable = true; @@ -27,7 +32,8 @@ systemd.enable = false; plugins = []; settings = {}; extraConfig = ""; enable = true; ## Use NixGL - package = config.lib.nixGL.wrap pkgs.hyprland; + #package = config.lib.nixGL.wrap pkgs.hyprland; + package = pkgs.hyprland; }; home = { @@ -167,6 +173,7 @@ songrec #songrec translate-shell #translate-shell wlogout #wlogout + libqalculate #libqalculate ] ++ [ @@ -174,7 +181,9 @@ ### illogical-impulse-quickshell-git #(config.lib.nixGL.wrap quickshell.packages.x86_64-linux.default) - (import ./quickshell.nix { inherit pkgs quickshell; nixGLWrap = config.lib.nixGL.wrap; }) + (import ./quickshell.nix { inherit pkgs quickshell; + #nixGLWrap = config.lib.nixGL.wrap; + }) ]; }//home_attrs; } diff --git a/sdata/dist-nix/home-manager/quickshell.nix b/sdata/dist-nix/home-manager/quickshell.nix index 0fb7b57fe..fb81af848 100644 --- a/sdata/dist-nix/home-manager/quickshell.nix +++ b/sdata/dist-nix/home-manager/quickshell.nix @@ -1,10 +1,14 @@ -{ pkgs, quickshell, nixGLWrap, ... }: +{ pkgs, quickshell, +#nixGLWrap, +... }: let - qs = nixGLWrap quickshell.packages.x86_64-linux.default; + #qs = nixGLWrap quickshell.packages.x86_64-linux.default; + qs = quickshell.packages.x86_64-linux.default; in pkgs.stdenv.mkDerivation { name = "illogical-impulse-quickshell-wrapper"; meta = with pkgs.lib; { - description = "Quickshell wrapped with NixGL + bundled Qt deps for home-manager usage"; + #description = "Quickshell wrapped with NixGL + bundled Qt deps for home-manager usage"; + description = "Quickshell bundled Qt deps for home-manager usage"; license = licenses.gpl3Only; }; diff --git a/sdata/dist-nix/install-deps.sh b/sdata/dist-nix/install-deps.sh index 357bb498f..2049efb82 100644 --- a/sdata/dist-nix/install-deps.sh +++ b/sdata/dist-nix/install-deps.sh @@ -32,8 +32,8 @@ function install_home-manager(){ try source $HOME/.nix-profile/etc/profile.d/hm-session-vars.sh command -v $cmd && return - x nix-channel --add https://nixos.org/channels/nixos-25.05 nixpkgs-home - x nix-channel --add https://github.com/nix-community/home-manager/archive/release-25.05.tar.gz home-manager + x nix-channel --add https://nixos.org/channels/nixos-25.11 nixpkgs-home + x nix-channel --add https://github.com/nix-community/home-manager/archive/release-25.11.tar.gz home-manager x nix-channel --update x env NIX_PATH="nixpkgs=$HOME/.nix-defexpr/channels/nixpkgs-home" nix-shell '' -A install @@ -56,6 +56,7 @@ function hm_deps(){ x home-manager switch --flake .#illogical_impulse \ --extra-experimental-features nix-command \ --extra-experimental-features flakes + x sudo /nix/store/*-non-nixos-gpu/bin/non-nixos-gpu-setup cd $REPO_ROOT x git rm -f "${SETUP_USERNAME_NIXFILE}" } diff --git a/sdata/lib/functions.sh b/sdata/lib/functions.sh index b6dcae99d..423f45b81 100644 --- a/sdata/lib/functions.sh +++ b/sdata/lib/functions.sh @@ -64,7 +64,7 @@ function showfun(){ function pause(){ if [ ! "$ask" == "false" ];then printf "${STY_FAINT}${STY_SLANT}" - local p; read -p "(Ctrl-C to abort, others to proceed)" p + local p; read -p "(Ctrl-C to abort, Enter to proceed)" p printf "${STY_RST}" fi } @@ -80,6 +80,50 @@ function prevent_sudo_or_root(){ root) echo -e "${STY_RED}[$0]: This script is NOT to be executed with sudo or as root. Aborting...${STY_RST}";exit 1;; esac } + +# Initialize sudo session and keep it alive in background +# Store PID in a global variable that can be accessed by trap +declare -g SUDO_KEEPALIVE_PID="" + +function sudo_init_keepalive(){ + # Check if sudo is available + if ! command -v sudo >/dev/null 2>&1; then + return 0 + fi + + # Skip if already initialized + if [[ -n "$SUDO_KEEPALIVE_PID" ]] && kill -0 "$SUDO_KEEPALIVE_PID" 2>/dev/null; then + return 0 + fi + + # Prompt for sudo password once at the beginning + echo -e "${STY_CYAN}[$0]: Requesting sudo privileges for installation...${STY_RST}" + if ! sudo -v; then + echo -e "${STY_RED}[$0]: Failed to obtain sudo privileges. Aborting...${STY_RST}" + exit 1 + fi + + # Start background process to keep sudo session alive + # This updates the sudo timestamp every 60 seconds + ( + while true; do + sleep 60 + sudo -v 2>/dev/null || exit 0 + done + ) & + SUDO_KEEPALIVE_PID=$! + + echo -e "${STY_GREEN}[$0]: Sudo session initialized and will be kept alive (PID: $SUDO_KEEPALIVE_PID)${STY_RST}" +} + +# Stop the sudo keepalive background process +function sudo_stop_keepalive(){ + if [[ -n "$SUDO_KEEPALIVE_PID" ]] && kill -0 "$SUDO_KEEPALIVE_PID" 2>/dev/null; then + kill "$SUDO_KEEPALIVE_PID" 2>/dev/null + wait "$SUDO_KEEPALIVE_PID" 2>/dev/null + SUDO_KEEPALIVE_PID="" + fi +} function git_auto_unshallow(){ # We need this function for latest_commit_hash to work properly if [[ -f "$(git rev-parse --git-dir)/shallow" ]]; then diff --git a/sdata/subcmd-exp-update/0.run.sh b/sdata/subcmd-exp-update/0.run.sh index 2ab21379c..72c11e770 100644 --- a/sdata/subcmd-exp-update/0.run.sh +++ b/sdata/subcmd-exp-update/0.run.sh @@ -31,9 +31,7 @@ # set -euo pipefail -# TODO: For Arch(-Linux) specific part please check if pacman exists first, if not it should be skipped. - -# TODO: Is this really needed? `git pull` should do a full upgrade, not partially, which means this script will be updated along with the folder structure together. +# Note: The detect_repo_structure function below auto-detects the folder layout # Try to find the packages directory (different names in different versions) if which pacman &>/dev/null; then if [[ -d "${REPO_ROOT}/dist-arch" ]]; then @@ -56,7 +54,6 @@ declare -a IGNORE_SUBSTRING_PATTERNS=() # Track created directories to avoid redundant mkdir calls declare -A CREATED_DIRS -# TODO: Is this really needed? `git pull` should do a full upgrade, not partially, which means this script will be updated along with the folder structure together. # Auto-detect repository structure detect_repo_structure() { local found_dirs=() @@ -644,23 +641,41 @@ build_packages() { log_info "Building package: $pkg_name" if [[ "$DRY_RUN" == true ]]; then - log_info "[DRY-RUN] Would build package in directory: $pkg_dir" + log_info "[DRY-RUN] Would build package in temp directory and clean up after" continue fi - cd "$pkg_dir" || { - log_error "Failed to change to package directory: $pkg_dir" + # Create temp build directory to avoid polluting the repo + local build_tmp_dir + build_tmp_dir=$(mktemp -d "/tmp/pkgbuild-${pkg_name}-XXXXXX") + + # Copy package files to temp directory (using /. to include hidden files) + cp -r "$pkg_dir"/. "$build_tmp_dir/" || { + log_error "Failed to copy package files to temp directory" + rm -rf "$build_tmp_dir" continue } - if makepkg -si --noconfirm; then + cd "$build_tmp_dir" || { + log_error "Failed to change to temp build directory: $build_tmp_dir" + rm -rf "$build_tmp_dir" + continue + } + + if makepkg -sCi --noconfirm; then log_success "Successfully built and installed $pkg_name" - ((rebuilt_packages++)) + ((rebuilt_packages++)) || true else log_error "Failed to build package $pkg_name" fi + # Clean up temp build directory cd "$REPO_ROOT" || log_die "Failed to return to repository directory" + rm -rf "$build_tmp_dir" + log_info "Cleaned up temp build directory" + + # Also clean any old build artifacts in the original package directory + rm -rf "${pkg_dir}/src" "${pkg_dir}/pkg" "${pkg_dir}"/*.pkg.tar.* 2>/dev/null || true done if [[ $rebuilt_packages -eq 0 ]]; then @@ -852,8 +867,12 @@ if git remote get-url origin &>/dev/null; then fi fi else - log_warning "Failed to pull changes from remote. Continuing with local repository..." - log_info "You may need to resolve conflicts manually later." + log_warning "Failed to pull changes from remote." + log_warning "This could be due to:" + log_warning " - Network issues" + log_warning " - Uncommitted local changes (use 'git stash' first)" + log_warning " - Diverged history (may need 'git pull --rebase')" + log_info "Continuing with local repository state..." fi fi else @@ -879,68 +898,70 @@ if [[ "$CHECK_PACKAGES" == true ]]; then if [[ "$PKG_TOOLS_AVAILABLE" == true ]]; then if [[ ! -d "$ARCH_PACKAGES_DIR" ]]; then - log_warning "No packages directory found (tried: dist-arch, arch-packages, sdata/dist-arch). Skipping package management." + log_warning "No packages directory found (tried: dist-arch, arch-packages, sdata/dist-arch)." + log_warning "Skipping package management." else - changed_pkgbuilds=() - for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do - if [[ -f "${pkg_dir}/PKGBUILD" ]]; then - pkg_name=$(basename "$pkg_dir") - if check_pkgbuild_changed "$pkg_dir"; then - changed_pkgbuilds+=("$pkg_name") - fi - fi - done - - if [[ ${#changed_pkgbuilds[@]} -gt 0 ]]; then - log_info "Found ${#changed_pkgbuilds[@]} package(s) with changed PKGBUILDs: ${changed_pkgbuilds[*]}" - echo - echo "Package build options:" - echo "1) Build only packages with changed PKGBUILDs" - echo "2) List all packages and select which to build" - echo "3) Build all packages" - echo "4) Skip package building" - echo - - if [[ "$NON_INTERACTIVE" == true ]]; then - pkg_choice="1" - log_info "Non-interactive mode: Using default package option: $pkg_choice" - elif safe_read "Choose an option (1-4): " pkg_choice "1"; then - if [[ "$VERBOSE" == true ]]; then - log_info "User selected package option: $pkg_choice" - fi - else - log_warning "Failed to read input. Skipping package building." - pkg_choice="" - fi - - if [[ -n "$pkg_choice" ]]; then - case $pkg_choice in - 1) build_packages "changed" ;; - 2) - if list_packages; then - build_packages "select" + # Scan for changed PKGBUILDs + changed_pkgbuilds=() + for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do + if [[ -f "${pkg_dir}/PKGBUILD" ]]; then + pkg_name=$(basename "$pkg_dir") + if check_pkgbuild_changed "$pkg_dir"; then + changed_pkgbuilds+=("$pkg_name") fi - ;; - 3) build_packages "all" ;; - 4 | *) log_info "Skipping package building" ;; - esac - fi - else - log_info "No PKGBUILDs have changed since last update." - echo - if [[ "$NON_INTERACTIVE" == true ]]; then - check_anyway="N" - log_info "Non-interactive mode: Using default for check packages anyway: $check_anyway" - elif safe_read "Do you want to check and build packages anyway? (y/N): " check_anyway "N"; then - if [[ "$VERBOSE" == true ]]; then - log_info "User chose to check packages anyway: $check_anyway" + fi + done + + if [[ ${#changed_pkgbuilds[@]} -gt 0 ]]; then + log_info "Found ${#changed_pkgbuilds[@]} package(s) with changed PKGBUILDs: ${changed_pkgbuilds[*]}" + echo + echo "Package build options:" + echo "1) Build only packages with changed PKGBUILDs" + echo "2) List all packages and select which to build" + echo "3) Build all packages" + echo "4) Skip package building" + echo + + if [[ "$NON_INTERACTIVE" == true ]]; then + pkg_choice="1" + log_info "Non-interactive mode: Using default package option: $pkg_choice" + elif safe_read "Choose an option (1-4): " pkg_choice "1"; then + if [[ "$VERBOSE" == true ]]; then + log_info "User selected package option: $pkg_choice" + fi + else + log_warning "Failed to read input. Skipping package building." + pkg_choice="" + fi + + if [[ -n "$pkg_choice" ]]; then + case $pkg_choice in + 1) build_packages "changed" ;; + 2) + if list_packages; then + build_packages "select" + fi + ;; + 3) build_packages "all" ;; + 4|*) log_info "Skipping package building" ;; + esac fi else - log_warning "Failed to read input. Skipping package management." - check_anyway="" - fi + log_info "No PKGBUILDs have changed since last update." + echo + if [[ "$NON_INTERACTIVE" == true ]]; then + check_anyway="N" + log_info "Non-interactive mode: Using default for check packages anyway: $check_anyway" + elif safe_read "Do you want to check and build packages anyway? (y/N): " check_anyway "N"; then + if [[ "$VERBOSE" == true ]]; then + log_info "User chose to check packages anyway: $check_anyway" + fi + else + log_warning "Failed to read input. Skipping package management." + check_anyway="" + fi - if [[ -n "$check_anyway" && "$check_anyway" =~ ^[Yy]$ ]]; then + if [[ -n "$check_anyway" && "$check_anyway" =~ ^[Yy]$ ]]; then if list_packages; then echo echo "Package build options:" @@ -950,9 +971,9 @@ if [[ "$CHECK_PACKAGES" == true ]]; then if safe_read "Choose an option (1-3): " build_choice "3"; then case $build_choice in - 1) build_packages "select" ;; - 2) build_packages "all" ;; - 3 | *) log_info "Skipping package building" ;; + 1) build_packages "select" ;; + 2) build_packages "all" ;; + 3|*) log_info "Skipping package building" ;; esac else log_info "Skipping package building" @@ -963,10 +984,10 @@ if [[ "$CHECK_PACKAGES" == true ]]; then fi fi fi - else - log_header "Package Management" - log_info "Package checking disabled. Use -p or --packages flag to enable package management." fi +else + log_header "Package Management" + log_info "Package checking disabled. Use -p or --packages flag to enable package management." fi # Step 3: Update configuration files diff --git a/sdata/subcmd-install/3.files-exp.yaml b/sdata/subcmd-install/3.files-exp.yaml index 5282e0594..63a0cad1f 100644 --- a/sdata/subcmd-install/3.files-exp.yaml +++ b/sdata/subcmd-install/3.files-exp.yaml @@ -20,6 +20,7 @@ patterns: - from: "dots/.config/fish" to: "$XDG_CONFIG_HOME/fish" mode: "sync" + excludes: ["conf.d"] condition: type: "shell" value: "fish" diff --git a/sdata/subcmd-install/3.files-legacy.sh b/sdata/subcmd-install/3.files-legacy.sh index 6ba8cefc2..0a3bef85f 100644 --- a/sdata/subcmd-install/3.files-legacy.sh +++ b/sdata/subcmd-install/3.files-legacy.sh @@ -30,7 +30,7 @@ esac case "${SKIP_FISH}" in true) sleep 0;; *) - install_dir__sync dots/.config/fish "$XDG_CONFIG_HOME"/fish + install_dir__sync_exclude dots/.config/fish "$XDG_CONFIG_HOME"/fish "conf.d" ;; esac diff --git a/sdata/subcmd-install/3.files.sh b/sdata/subcmd-install/3.files.sh index 41e9972d0..57c8568e1 100644 --- a/sdata/subcmd-install/3.files.sh +++ b/sdata/subcmd-install/3.files.sh @@ -67,6 +67,22 @@ rsync_dir__sync(){ x mkdir -p "$(dirname ${INSTALLED_LISTFILE})" rsync -a --delete --out-format='%i %n' "$1"/ "$2"/ | awk -v d="$dest" '$1 ~ /^>/{ sub(/^[^ ]+ /,""); printf d "/" $0 "\n" }' >> "${INSTALLED_LISTFILE}" } +rsync_dir__sync_exclude(){ + # NOTE: This function is only for using in other functions + # Same as rsync_dir__sync but with exclude patterns support + # Usage: rsync_dir__sync_exclude [ ...] + local src="$1" + local dest_dir="$2" + shift 2 + local excludes=() + for pattern in "$@"; do + excludes+=(--exclude "$pattern") + done + x mkdir -p "$dest_dir" + local dest="$(realpath -se $dest_dir)" + x mkdir -p "$(dirname ${INSTALLED_LISTFILE})" + rsync -a --delete "${excludes[@]}" --out-format='%i %n' "$src"/ "$dest_dir"/ | awk -v d="$dest" '$1 ~ /^>/{ sub(/^[^ ]+ /,""); printf d "/" $0 "\n" }' >> "${INSTALLED_LISTFILE}" +} function install_file(){ # NOTE: Do not add prefix `v` or `x` when using this function local s=$1 @@ -124,6 +140,18 @@ function install_dir__skip_existed(){ v rsync_dir $s $t fi } +function install_dir__sync_exclude(){ + # NOTE: Do not add prefix `v` or `x` when using this function + # Sync directory with exclude patterns + # Usage: install_dir__sync_exclude [ ...] + local s=$1 + local t=$2 + shift 2 + if [ -d $t ];then + warning_overwrite + fi + rsync_dir__sync_exclude $s $t "$@" +} function install_google_sans_flex(){ local font_name="Google Sans Flex" local src_name="google-sans-flex" @@ -142,7 +170,7 @@ function install_google_sans_flex(){ x fc-cache -fv x cd $REPO_ROOT x mkdir -p "$(dirname ${INSTALLED_LISTFILE})" - realpath -se "$2" >> "${INSTALLED_LISTFILE}" + realpath -se "$target_dir" >> "${INSTALLED_LISTFILE}" } ##################################################################################### diff --git a/setup b/setup index 8a6e2d82f..9174eeb93 100755 --- a/setup +++ b/setup @@ -71,6 +71,10 @@ case ${SUBCMD_NAME} in $function done pause + # Initialize sudo keepalive for the entire install process + sudo_init_keepalive + # Set trap to cleanup when this subcommand exits + trap sudo_stop_keepalive EXIT INT TERM if [[ "${SKIP_ALLGREETING}" != true ]]; then source ${SUBCMD_DIR}/0.greeting.sh fi @@ -89,6 +93,10 @@ case ${SUBCMD_NAME} in $function done pause + # Initialize sudo keepalive for dependency installation + sudo_init_keepalive + # Set trap to cleanup when this subcommand exits + trap sudo_stop_keepalive EXIT INT TERM if [[ "${SKIP_ALLDEPS}" != true ]]; then source ${SUBCMD_DIR}/1.deps-router.sh fi @@ -98,6 +106,10 @@ case ${SUBCMD_NAME} in $function done pause + # Initialize sudo keepalive for setup steps + sudo_init_keepalive + # Set trap to cleanup when this subcommand exits + trap sudo_stop_keepalive EXIT INT TERM if [[ "${SKIP_ALLSETUPS}" != true ]]; then source ${SUBCMD_DIR}/2.setups.sh fi